最新ES6提案

一、do 表达式

本质上,块级作用域是一个语句,将多个操作封装在一起,没有返回值。

{
  let t = f();
  t = t * t + 1;
}

上面代码中,块级作用域将两个语句封装在一起。但是,在块级作用域以外,没有办法得到t的值,因为块级作用域不返回值,除非t是全局变量。
现在有一个提案,使得块级作用域可以变为表达式,也就是说可以返回值,办法就是在块级作用域之前加上do,使它变为do表达式,然后就会返回内部最后执行的表达式的值。

let x = do {
  let t = f();
  t * t + 1;
};

上面代码中,变量x会得到整个块级作用域的返回值(t * t + 1)。

do表达式的逻辑非常简单:封装的是什么,就会返回什么。

// 等同于 <表达式>
do { <表达式>; }

// 等同于 <语句>
do { <语句> }

do表达式的好处是可以封装多个语句,让程序更加模块化,就像乐高积木那样一块块拼装起来。

let x = do {
  if (foo()) { f() }
  else if (bar()) { g() }
  else { h() }
};

上面代码的本质,就是根据函数foo的执行结果,调用不同的函数,将返回结果赋给变量x。使用do表达式,就将这个操作的意图表达得非常简洁清晰。而且,do块级作用域提供了单独的作用域,内部操作可以与全局作用域隔绝。

值得一提的是,do表达式在 JSX 语法中非常好用。

return (
  <nav>
    <Home />
    {
      do {
        if (loggedIn) {
          <LogoutButton />
        } else {
          <LoginButton />
        }
      }
    }
  </nav>
)

上面代码中,如果不用do表达式,就只能用三元判断运算符(?:)。那样的话,一旦判断逻辑复杂,代码就会变得很不易读。

二、throw 表达式

JavaScript 语法规定throw是一个命令,用来抛出错误,不能用于表达式之中。

// 报错
console.log(throw new Error());

上面代码中,console.log的参数必须是一个表达式,如果是一个throw语句就会报错。

现在有一个提案,允许throw用于表达式。

// 参数的默认值
function save(filename = throw new TypeError("Argument required")) {
}

// 箭头函数的返回值
lint(ast, {
  with: () => throw new Error("avoid using 'with' statements.")
});

// 条件表达式
function getEncoder(encoding) {
  const encoder = encoding === "utf8" ?
    new UTF8Encoder() :
    encoding === "utf16le" ?
      new UTF16Encoder(false) :
      encoding === "utf16be" ?
        new UTF16Encoder(true) :
        throw new Error("Unsupported encoding");
}

// 逻辑表达式
class Product {
  get id() {
    return this._id;
  }
  set id(value) {
    this._id = value || throw new Error("Invalid value");
  }
}

上面代码中,throw都出现在表达式里面。

语法上,throw表达式里面的throw不再是一个命令,而是一个运算符。为了避免与throw命令混淆,规定throw出现在行首,一律解释为throw语句,而不是throw表达式。

三、函数的部分执行

多参数的函数有时需要绑定其中的一个或多个参数,然后返回一个新函数。

function add(x, y) { return x + y; }
function add7(x) { return x + 7; }

上面代码中,add7函数其实是add函数的一个特殊版本,通过将一个参数绑定为7,就可以从add得到add7。

// bind 方法
const add7 = add.bind(null, 7);

// 箭头函数
const add7 = x => add(x, 7);

上面两种写法都有些冗余。其中,bind方法的局限更加明显,它必须提供this,并且只能从前到后一个个绑定参数,无法只绑定非头部的参数。

现在有一个提案,使得绑定参数并返回一个新函数更加容易。这叫做函数的部分执行(partial application)。

const add = (x, y) => x + y;
const addOne = add(1, ?);

const maxGreaterThanZero = Math.max(0, ...);

根据新提案,?是单个参数的占位符,…是多个参数的占位符。以下的形式都属于函数的部分执行。

f(x, ?)
f(x, ...)
f(?, x)
f(..., x)
f(?, x, ?)
f(..., x, ...)

?和…只能出现在函数的调用之中,并且会返回一个新函数。

const g = f(?, 1, ...);
// 等同于
const g = (x, ...y) => f(x, 1, ...y);

函数的部分执行,也可以用于对象的方法。

let obj = {
  f(x, y) { return x + y; },
};

const g = obj.f(?, 3);
g(1) // 4

四、管道运算符

Unix 操作系统有一个管道机制(pipeline),可以把前一个操作的值传给后一个操作。这个机制非常有用,使得简单的操作可以组合成为复杂的操作。许多语言都有管道的实现,现在有一个提案,让 JavaScript 也拥有管道机制。

JavaScript 的管道是一个运算符,写作|>。它的左边是一个表达式,右边是一个函数。管道运算符把左边表达式的值,传入右边的函数进行求值。

x |> f
// 等同于
f(x)

管道运算符最大的好处,就是可以把嵌套的函数,写成从左到右的链式表达式。

function doubleSay (str) {
  return str + ", " + str;
}

function capitalize (str) {
  return str[0].toUpperCase() + str.substring(1);
}

function exclaim (str) {
  return str + '!';
}

上面是三个简单的函数。如果要嵌套执行,传统的写法和管道的写法分别如下。

// 传统的写法
exclaim(capitalize(doubleSay('hello')))
// "Hello, hello!"

// 管道的写法
'hello'
  |> doubleSay
  |> capitalize
  |> exclaim
// "Hello, hello!"

管道运算符只能传递一个值,这意味着它右边的函数必须是一个单参数函数。如果是多参数函数,就必须进行柯里化,改成单参数的版本。

function double (x) { return x + x; }
function add (x, y) { return x + y; }

let person = { score: 25 };
person.score
  |> double
  |> (_ => add(7, _))
// 57

上面代码中,add函数需要两个参数。但是,管道运算符只能传入一个值,因此需要事先提供另一个参数,并将其改成单参数的箭头函数_ => add(7, _)。这个函数里面的下划线并没有特别的含义,可以用其他符号代替,使用下划线只是因为,它能够形象地表示这里是占位符。

管道运算符对于await函数也适用。

x |> await f
// 等同于
await f(x)

const userAge = userId |> await fetchUserById |> getAgeFromUser;
// 等同于
const userAge = getAgeFromUser(await fetchUserById(userId));

五、Math.signbit()

Math.sign()用来判断一个值的正负,但是如果参数是-0,它会返回-0。

Math.sign(-0) // -0

这导致对于判断符号位的正负,Math.sign()不是很有用。JavaScript 内部使用 64 位浮点数(国际标准 IEEE 754)表示数值,IEEE 754 规定第一位是符号位,0表示正数,1表示负数。所以会有两种零,+0是符号位为0时的零值,-0是符号位为1时的零值。实际编程中,判断一个值是+0还是-0非常麻烦,因为它们是相等的。

+0 === -0 // true

目前,有一个提案,引入了Math.signbit()方法判断一个数的符号位是否设置了。

Math.signbit(2) //false
Math.signbit(-2) //true
Math.signbit(0) //false
Math.signbit(-0) //true

可以看到,该方法正确返回了-0的符号位是设置了的。

该方法的算法如下。

  • 如果参数是NaN,返回false
  • 如果参数是-0,返回true
  • 如果参数是负值,返回true
  • 其他情况返回false

六、双冒号运算符

箭头函数可以绑定this对象,大大减少了显式绑定this对象的写法(call、apply、bind)。但是,箭头函数并不适用于所有场合,所以现在有一个提案,提出了“函数绑定”(function bind)运算符,用来取代call、apply、bind调用。

函数绑定运算符是并排的两个冒号(::),双冒号左边是一个对象,右边是一个函数。该运算符会自动将左边的对象,作为上下文环境(即this对象),绑定到右边的函数上面。

foo::bar;
// 等同于
bar.bind(foo);

foo::bar(...arguments);
// 等同于
bar.apply(foo, arguments);

const hasOwnProperty = Object.prototype.hasOwnProperty;
function hasOwn(obj, key) {
  return obj::hasOwnProperty(key);
}

如果双冒号左边为空,右边是一个对象的方法,则等于将该方法绑定在该对象上面。

var method = obj::obj.foo;
// 等同于
var method = ::obj.foo;

let log = ::console.log;
// 等同于
var log = console.log.bind(console);

如果双冒号运算符的运算结果,还是一个对象,就可以采用链式写法。

import { map, takeWhile, forEach } from "iterlib";

getPlayers()
::map(x => x.character())
::takeWhile(x => x.strength > 100)
::forEach(x => console.log(x));
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值