1
0
mirror of https://github.com/ruanyf/es6tutorial.git synced 2025-05-24 18:32:22 +00:00
es6tutorial/docs/proposals.md
2018-02-08 18:51:42 +08:00

584 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 最新提案
本章介绍一些尚未进入标准、但很有希望的最新提案。
## do 表达式
本质上,块级作用域是一个语句,将多个操作封装在一起,没有返回值。
```javascript
{
let t = f();
t = t * t + 1;
}
```
上面代码中,块级作用域将两个语句封装在一起。但是,在块级作用域以外,没有办法得到`t`的值,因为块级作用域不返回值,除非`t`是全局变量。
现在有一个[提案](https://github.com/tc39/proposal-do-expressions),使得块级作用域可以变为表达式,也就是说可以返回值,办法就是在块级作用域之前加上`do`,使它变为`do`表达式,然后就会返回内部最后执行的表达式的值。
```javascript
let x = do {
let t = f();
t * t + 1;
};
```
上面代码中,变量`x`会得到整个块级作用域的返回值(`t * t + 1`)。
`do`表达式的逻辑非常简单:封装的是什么,就会返回什么。
```javascript
// 等同于 <表达式>
do { <表达式>; }
// 等同于 <语句>
do { <语句> }
```
`do`表达式的好处是可以封装多个语句,让程序更加模块化,就像乐高积木那样一块块拼装起来。
```javascript
let x = do {
if (foo()) { f() }
else if (bar()) { g() }
else { h() }
};
```
上面代码的本质,就是根据函数`foo`的执行结果,调用不同的函数,将返回结果赋给变量`x`。使用`do`表达式,就将这个操作的意图表达得非常简洁清晰。而且,`do`块级作用域提供了单独的作用域,内部操作可以与全局作用域隔绝。
值得一提的是,`do`表达式在 JSX 语法中非常好用。
```javascript
return (
<nav>
<Home />
{
do {
if (loggedIn) {
<LogoutButton />
} else {
<LoginButton />
}
}
}
</nav>
)
```
上面代码中,如果不用`do`表达式,就只能用三元判断运算符(`?:`)。那样的话,一旦判断逻辑复杂,代码就会变得很不易读。
## throw 表达式
JavaScript 语法规定`throw`是一个命令,用来抛出错误,不能用于表达式之中。
```javascript
// 报错
console.log(throw new Error());
```
上面代码中,`console.log`的参数必须是一个表达式,如果是一个`throw`语句就会报错。
现在有一个[提案](https://github.com/tc39/proposal-throw-expressions),允许`throw`用于表达式。
```javascript
// 参数的默认值
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`表达式。
## Null 传导运算符
编程实务中,如果读取对象内部的某个属性,往往需要判断一下该对象是否存在。比如,要读取`message.body.user.firstName`,安全的写法是写成下面这样。
```javascript
const firstName = (message
&& message.body
&& message.body.user
&& message.body.user.firstName) || 'default';
```
这样的层层判断非常麻烦,因此现在有一个[提案](https://github.com/claudepache/es-optional-chaining)引入了“Null 传导运算符”null propagation operator`?.`,简化上面的写法。
```javascript
const firstName = message?.body?.user?.firstName || 'default';
```
上面代码有三个`?.`运算符,只要其中一个返回`null``undefined`,就不再往下运算,而是返回`undefined`
“Null 传导运算符”有四种用法。
- `obj?.prop` // 读取对象属性
- `obj?.[expr]` // 同上
- `func?.(...args)` // 函数或对象方法的调用
- `new C?.(...args)` // 构造函数的调用
传导运算符之所以写成`obj?.prop`,而不是`obj?prop`,是为了方便编译器能够区分三元运算符`?:`(比如`obj?prop:123`)。
下面是更多的例子。
```javascript
// 如果 a 是 null 或 undefined, 返回 undefined
// 否则返回 a.b.c().d
a?.b.c().d
// 如果 a 是 null 或 undefined下面的语句不产生任何效果
// 否则执行 a.b = 42
a?.b = 42
// 如果 a 是 null 或 undefined下面的语句不产生任何效果
delete a?.b
```
## 直接输入 U+2028 和 U+2029
JavaScript 字符串允许直接输入字符,以及输入字符的转义形式。举例来说,“中”的 Unicode 码点是 U+4e2d你可以直接在字符串里面输入这个汉字也可以输入它的转义形式`\u4e2d`,两者是等价的。
```javascript
'中' === '\u4e2d' // true
```
但是JavaScript 规定有5个字符不能在字符串里面直接使用只能使用转义形式。
- U+005C反斜杠reverse solidus)
- U+000D回车carriage return
- U+2028行分隔符line separator
- U+2029段分隔符paragraph separator
- U+000A换行符line feed
举例来说,字符串里面不能直接包含反斜杠,一定要转义写成`\\`或者`\u005c`
这个规定本身没有问题,麻烦在于 JSON 格式允许字符串里面直接使用 U+2028行分隔符和 U+2029段分隔符。这样一来服务器输出的 JSON 被`JSON.parse`解析,就有可能直接报错。
JSON 格式已经冻结RFC 7159没法修改了。为了消除这个报错现在有一个[提案](https://github.com/tc39/proposal-json-superset),允许 JavaScript 字符串直接输入 U+2028行分隔符和 U+2029段分隔符
```javascript
const PS = eval("'\u2029'");
```
根据这个提案,上面的代码不会报错。
注意,模板字符串现在就允许直接输入这两个字符。另外,正则表达式依然不允许直接输入这两个字符,这是没有问题的,因为 JSON 本来就不允许直接包含正则表达式。
## 函数的部分执行
### 语法
多参数的函数有时需要绑定其中的一个或多个函数,然后返回一个新函数。
```javascript
function add(x, y) { return x + y; }
function add7(x) { return x + 7; }
```
上面代码中,`add7`函数其实是`add`函数的一个特殊版本,通过将一个参数绑定为`7`,就可以从`add`得到`add7`
```javascript
// bind 方法
const add7 = add.bind(null, 7);
// 箭头函数
const add7 = x => add(x, 7);
```
上面两种写法都有些冗余。其中,`bind`方法的局限更加明显,它必须提供`this`,并且只能从前到后一个个绑定参数,无法只绑定非头部的参数。
现在有一个[提案](https://github.com/tc39/proposal-partial-application)使用绑定参数然后返回一个新函数更加容器。这叫做函数的部分执行partial application
```javascript
const add = (x, y) => x + y;
const addOne = add(1, ?);
const maxGreaterThanZero = Math.max(0, ...);
```
根据新提案,`?`是单个参数的占位符,`...`是多个参数的占位符。以下的形式都属于函数的部分执行。
```javascript
f(x, ?)
f(x, ...)
f(?, x)
f(..., x)
f(?, x, ?)
f(..., x, ...)
```
`?``...`只能出现在函数的调用之中,并且会返回一个新函数。
```javascript
const g = f(?, 1, ...);
// 等同于
const g = (x, ...y) => f(x, 1, ...y);
```
函数的部分执行,也可以用于对象的方法。
```javascript
let obj = {
f(x, y) { return x + y; },
};
const g = obj.f(?, 3);
g(1) // 4
```
### 注意点
函数的部分执行有一些特别注意的地方。
1函数的部分执行是基于原函数的。如果原函数发生变化部分执行生成的新函数也会立即反映这种变化。
```javascript
let f = (x, y) => x + y;
const g = f(?, 3);
g(1); // 4
// 替换函数 f
f = (x, y) => x * y;
g(1); // 3
```
上面代码中,定义了函数的部分执行以后,更换原函数会立即影响到新函数。
2如果预先提供的那个值是一个表达式那么这个表达式并不会在定义时求值而是在每次调用时求值。
```javascript
let a = 3;
const f = (x, y) => x + y;
const g = f(?, a);
g(1); // 4
// 改变 a 的值
a = 10;
g(1); // 11
```
上面代码中,预先提供的参数是变量`a`,那么每次调用函数`g`的时候,才会对`a`进行求值。
3如果新函数的参数多于占位符的数量那么多余的参数将被忽略。
```javascript
const f = (x, ...y) => [x, ...y];
const g = f(?, 1);
g(2, 3, 4); // [2, 1]
```
上面代码中,函数`g`只有一个占位符,也就意味着它只能接受一个参数,多余的参数都会被忽略。
写成下面这样,多余的参数就没有问题。
```javascript
const f = (x, ...y) => [x, ...y];
const g = f(?, 1, ...);
g(2, 3, 4); // [2, 1, 3, 4];
```
4`...`只会被采集一次,如果函数的部分执行使用了多个`...`,那么每个`...`的值都将相同。
```javascript
const f = (...x) => x;
const g = f(..., 9, ...);
g(1, 2, 3); // [1, 2, 3, 9, 1, 2, 3]
```
上面代码中,`g`定义了两个`...`占位符,真正执行的时候,它们的值是一样的。
## 管道运算符
Unix 操作系统有一个管道机制pipeline可以把前一个操作的值传给后一个操作。这个机制非常有用使得简单的操作可以组合成为复杂的操作。许多语言都有管道的实现现在有一个[提案](https://github.com/tc39/proposal-partial-application),让 JavaScript 也拥有管道机制。
JavaScript 的管道是一个运算符,写作`|>`。它的左边是一个表达式,右边是一个函数。管道运算符把左边表达式的值,传入右边的函数进行求值。
```javascript
x |> f
// 等同于
f(x)
```
管道运算符最大的好处,就是可以把嵌套的函数,写成从左到右的链式表达式。
```javascript
function doubleSay (str) {
return str + ", " + str;
}
function capitalize (str) {
return str[0].toUpperCase() + str.substring(1);
}
function exclaim (str) {
return str + '!';
}
```
上面是三个简单的函数。如果要嵌套执行,传统的写法和管道的写法分别如下。
```javascript
// 传统的写法
exclaim(capitalize(doubleSay('hello')))
// "Hello, hello!"
// 管道的写法
'hello'
|> doubleSay
|> capitalize
|> exclaim
// "Hello, hello!"
```
管道运算符只能传递一个值,这意味着它右边的函数必须是一个单参数函数。如果是多参数函数,就必须进行柯里化,改成单参数的版本。
```javascript
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`函数也适用。
```javascript
x |> await f
// 等同于
await f(x)
const userAge = userId |> await fetchUserById |> getAgeFromUser;
// 等同于
const userAge = getAgeFromUser(await fetchUserById(userId));
```
## 数值分隔符
欧美语言中,较长的数值允许每三位添加一个分隔符(通常是一个逗号),增加数值的可读性。比如,`1000`可以写作`1,000`
现在有一个[提案](https://github.com/tc39/proposal-numeric-separator),允许 JavaScript 的数值使用下划线(`_`)作为分隔符。
```javascript
let budget = 1_000_000_000_000;
budget === 10 ** 12 // true
```
JavaScript 的数值分隔符没有指定间隔的位数,也就是说,可以每三位添加一个分隔符,也可以每一位、每两位、每四位添加一个。
```javascript
123_00 === 12_300 // true
12345_00 === 123_4500 // true
12345_00 === 1_234_500 // true
```
小数和科学计数法也可以使用数值分隔符。
```javascript
// 小数
0.000_001
// 科学计数法
1e10_000
```
数值分隔符有几个使用注意点。
- 不能在数值的最前面leading或最后面trailing
- 不能两个或两个以上的分隔符连在一起。
- 小数点的前后不能有分隔符。
- 科学计数法里面,表示指数的`e``E`前后不能有分隔符。
下面的写法都会报错。
```javascript
// 全部报错
3_.141
3._141
1_e12
1e_12
123__456
_1464301
1464301_
```
除了十进制,其他进制的数值也可以使用分隔符。
```javascript
// 二进制
0b1010_0001_1000_0101
// 十六进制
0xA0_B0_C0
```
注意,分隔符不能紧跟着进制的前缀`0b``0B``0o``0O``0x``0X`
```javascript
// 报错
0_b111111000
0b_111111000
```
下面三个将字符串转成数值的函数,不支持数值分隔符。主要原因是提案的设计者认为,数值分隔符主要是为了编码时书写数值的方便,而不是为了处理外部输入的数据。
- Number()
- parseInt()
- parseFloat()
```javascript
Number('123_456') // NaN
parseInt('123_456') // 123
```
## Integer 数据类型
### 简介
JavaScript 所有数字都保存成 64 位浮点数,这决定了整数的精确程度只能到 53 个二进制位。大于这个范围的整数JavaScript 是无法精确表示的,这使得 JavaScript 不适合进行科学和金融方面的精确计算。
现在有一个[提案](https://github.com/tc39/proposal-bigint),引入了新的数据类型 Integer整数来解决这个问题。整数类型的数据只用来表示整数没有位数的限制任何位数的整数都可以精确表示。
为了与 Number 类型区别Integer 类型的数据必须使用后缀`n`表示。
```javascript
1n + 2n // 3n
```
二进制、八进制、十六进制的表示法,都要加上后缀`n`
```javascript
0b1101n // 二进制
0o777n // 八进制
0xFFn // 十六进制
```
`typeof`运算符对于 Integer 类型的数据返回`integer`
```javascript
typeof 123n
// 'integer'
```
JavaScript 原生提供`Integer`对象,用来生成 Integer 类型的数值。转换规则基本与`Number()`一致。
```javascript
Integer(123) // 123n
Integer('123') // 123n
Integer(false) // 0n
Integer(true) // 1n
```
以下的用法会报错。
```javascript
new Integer() // TypeError
Integer(undefined) //TypeError
Integer(null) // TypeError
Integer('123n') // SyntaxError
Integer('abc') // SyntaxError
```
### 运算
在数学运算方面Integer 类型的`+``-``*``**`这四个二元运算符,与 Number 类型的行为一致。除法运算`/`会舍去小数部分,返回一个整数。
```javascript
9n / 5n
// 1n
```
几乎所有的 Number 运算符都可以用在 Integer但是有两个除外不带符号的右移位运算符`>>>`和一元的求正运算符`+`,使用时会报错。前者是因为`>>>`要求最高位补 0但是 Integer 类型没有最高位,导致这个运算符无意义。后者是因为一元运算符`+`在 asm.js 里面总是返回 Number 类型或者报错。
Integer 类型不能与 Number 类型进行混合运算。
```javascript
1n + 1
// 报错
```
这是因为无论返回的是 Integer 或 Number都会导致丢失信息。比如`(2n**53n + 1n) + 0.5`这个表达式,如果返回 Integer 类型,`0.5`这个小数部分会丢失;如果返回 Number 类型,会超过 53 位精确数字,精度下降。
相等运算符(`==`)会改变数据类型,也是不允许混合使用。
```javascript
0n == 0
// 报错 TypeError
0n == false
// 报错 TypeError
```
精确相等运算符(`===`)不会改变数据类型,因此可以混合使用。
```javascript
0n === 0
// false
```
## Math.signbit()
`Math.sign()`用来判断一个值的正负,但是如果参数是`-0`,它会返回`-0`
```javascript
Math.sign(-0) // -0
```
这导致对于判断符号位的正负,`Math.sign()`不是很有用。JavaScript 内部使用 64 位浮点数(国际标准 IEEE 754表示数值IEEE 754 规定第一位是符号位,`0`表示正数,`1`表示负数。所以会有两种零,`+0`是符号位为`0`时的零值,`-0`是符号位为`1`时的零值。实际编程中,判断一个值是`+0`还是`-0`非常麻烦,因为它们是相等的。
```javascript
+0 === -0 // true
```
目前,有一个[提案](http://jfbastien.github.io/papers/Math.signbit.html),引入了`Math.signbit()`方法判断一个数的符号位是否设置了。
```javascript
Math.signbit(2) //false
Math.signbit(-2) //true
Math.signbit(0) //false
Math.signbit(-0) //true
```
可以看到,该方法正确返回了`-0`的符号位是设置了的。
该方法的算法如下。
- 如果参数是`NaN`,返回`false`
- 如果参数是`-0`,返回`true`
- 如果参数是负值,返回`true`
- 其他情况返回`false`