From fdac6ccb2e93dc4956e8b8ab10cdfaa912b4ca73 Mon Sep 17 00:00:00 2001 From: ruanyf Date: Sat, 10 Jul 2021 14:58:15 +0800 Subject: [PATCH] docs: add ES2021 --- docs/number.md | 124 ++++++++++++++----- docs/object.md | 229 ---------------------------------- docs/operator.md | 309 ++++++++++++++++++++++++++++++++++++++++++++++ docs/promise.md | 39 +++--- docs/proposals.md | 77 ------------ docs/set-map.md | 148 ++++++++++++++++++++++ 6 files changed, 571 insertions(+), 355 deletions(-) create mode 100644 docs/operator.md diff --git a/docs/number.md b/docs/number.md index 250e7e7..c21067e 100644 --- a/docs/number.md +++ b/docs/number.md @@ -31,6 +31,97 @@ Number('0b111') // 7 Number('0o10') // 8 ``` +## 数值分隔符 + +欧美语言中,较长的数值允许每三位添加一个分隔符(通常是一个逗号),增加数值的可读性。比如,`1000`可以写作`1,000`。 + +[ES2021](https://github.com/tc39/proposal-numeric-separator),允许 JavaScript 的数值使用下划线(`_`)作为分隔符。 + +```javascript +let budget = 1_000_000_000_000; +budget === 10 ** 12 // true +``` + +这个数值分隔符没有指定间隔的位数,也就是说,可以每三位添加一个分隔符,也可以每一位、每两位、每四位添加一个。 + +```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 +``` + +数值分隔符只是一种书写便利,对于 JavaScript 内部数值的存储和输出,并没有影响。 + +```javascript +let num = 12_345; + +num // 12345 +num.toString() // 12345 +``` + +上面示例中,变量`num`的值为`12_345`,但是内部存储和输出的时候,都不会有数值分隔符。 + +下面三个将字符串转成数值的函数,不支持数值分隔符。主要原因是语言的设计者认为,数值分隔符主要是为了编码时书写数值的方便,而不是为了处理外部输入的数据。 + +- Number() +- parseInt() +- parseFloat() + +```javascript +Number('123_456') // NaN +parseInt('123_456') // 123 +``` + ## Number.isFinite(), Number.isNaN() ES6 在`Number`对象上,新提供了`Number.isFinite()`和`Number.isNaN()`两个方法。 @@ -655,42 +746,11 @@ ES6 新增了 6 个双曲函数方法。 - `Math.acosh(x)` 返回`x`的反双曲余弦(inverse hyperbolic cosine) - `Math.atanh(x)` 返回`x`的反双曲正切(inverse hyperbolic tangent) -## 指数运算符 - -ES2016 新增了一个指数运算符(`**`)。 - -```javascript -2 ** 2 // 4 -2 ** 3 // 8 -``` - -这个运算符的一个特点是右结合,而不是常见的左结合。多个指数运算符连用时,是从最右边开始计算的。 - -```javascript -// 相当于 2 ** (3 ** 2) -2 ** 3 ** 2 -// 512 -``` - -上面代码中,首先计算的是第二个指数运算符,而不是第一个。 - -指数运算符可以与等号结合,形成一个新的赋值运算符(`**=`)。 - -```javascript -let a = 1.5; -a **= 2; -// 等同于 a = a * a; - -let b = 4; -b **= 3; -// 等同于 b = b * b * b; -``` - ## BigInt 数据类型 ### 简介 -JavaScript 所有数字都保存成 64 位浮点数,这给数值的表示带来了两大限制。一是数值的精度只能到 53 个二进制位(相当于 16 个十进制位),大于这个范围的整数,JavaScript 是无法精确表示的,这使得 JavaScript 不适合进行科学和金融方面的精确计算。二是大于或等于2的1024次方的数值,JavaScript 无法表示,会返回`Infinity`。 +JavaScript 所有数字都保存成 64 位浮点数,这给数值的表示带来了两大限制。一是数值的精度只能到 53 个二进制位(相当于 16 个十进制位),大于这个范围的整数,JavaScript 是无法精确表示,这使得 JavaScript 不适合进行科学和金融方面的精确计算。二是大于或等于2的1024次方的数值,JavaScript 无法表示,会返回`Infinity`。 ```javascript // 超过 53 个二进制位的数值,无法保持精度 diff --git a/docs/object.md b/docs/object.md index 5b8395b..d2965e8 100644 --- a/docs/object.md +++ b/docs/object.md @@ -704,232 +704,3 @@ let aWithXGetter = { ...a }; // 报错 上面例子中,取值函数`get`在扩展`a`对象时会自动执行,导致报错。 -## 链判断运算符 - -编程实务中,如果读取对象内部的某个属性,往往需要判断一下该对象是否存在。比如,要读取`message.body.user.firstName`,安全的写法是写成下面这样。 - -```javascript -// 错误的写法 -const firstName = message.body.user.firstName; - -// 正确的写法 -const firstName = (message - && message.body - && message.body.user - && message.body.user.firstName) || 'default'; -``` - -上面例子中,`firstName`属性在对象的第四层,所以需要判断四次,每一层是否有值。 - -三元运算符`?:`也常用于判断对象是否存在。 - -```javascript -const fooInput = myForm.querySelector('input[name=foo]') -const fooValue = fooInput ? fooInput.value : undefined -``` - -上面例子中,必须先判断`fooInput`是否存在,才能读取`fooInput.value`。 - -这样的层层判断非常麻烦,因此 [ES2020](https://github.com/tc39/proposal-optional-chaining) 引入了“链判断运算符”(optional chaining operator)`?.`,简化上面的写法。 - -```javascript -const firstName = message?.body?.user?.firstName || 'default'; -const fooValue = myForm.querySelector('input[name=foo]')?.value -``` - -上面代码使用了`?.`运算符,直接在链式调用的时候判断,左侧的对象是否为`null`或`undefined`。如果是的,就不再往下运算,而是返回`undefined`。 - -下面是判断对象方法是否存在,如果存在就立即执行的例子。 - -```javascript -iterator.return?.() -``` - -上面代码中,`iterator.return`如果有定义,就会调用该方法,否则`iterator.return`直接返回`undefined`,不再执行`?.`后面的部分。 - -对于那些可能没有实现的方法,这个运算符尤其有用。 - -```javascript -if (myForm.checkValidity?.() === false) { - // 表单校验失败 - return; -} -``` - -上面代码中,老式浏览器的表单可能没有`checkValidity`这个方法,这时`?.`运算符就会返回`undefined`,判断语句就变成了`undefined === false`,所以就会跳过下面的代码。 - -链判断运算符有三种用法。 - -- `obj?.prop` // 对象属性 -- `obj?.[expr]` // 同上 -- `func?.(...args)` // 函数或对象方法的调用 - -下面是`obj?.[expr]`用法的一个例子。 - -```bash -let hex = "#C0FFEE".match(/#([A-Z]+)/i)?.[1]; -``` - -上面例子中,字符串的`match()`方法,如果没有发现匹配会返回`null`,如果发现匹配会返回一个数组,`?.`运算符起到了判断作用。 - -下面是`?.`运算符常见形式,以及不使用该运算符时的等价形式。 - -```javascript -a?.b -// 等同于 -a == null ? undefined : a.b - -a?.[x] -// 等同于 -a == null ? undefined : a[x] - -a?.b() -// 等同于 -a == null ? undefined : a.b() - -a?.() -// 等同于 -a == null ? undefined : a() -``` - -上面代码中,特别注意后两种形式,如果`a?.b()`里面的`a.b`不是函数,不可调用,那么`a?.b()`是会报错的。`a?.()`也是如此,如果`a`不是`null`或`undefined`,但也不是函数,那么`a?.()`会报错。 - -使用这个运算符,有几个注意点。 - -(1)短路机制 - -`?.`运算符相当于一种短路机制,只要不满足条件,就不再往下执行。 - -```javascript -a?.[++x] -// 等同于 -a == null ? undefined : a[++x] -``` - -上面代码中,如果`a`是`undefined`或`null`,那么`x`不会进行递增运算。也就是说,链判断运算符一旦为真,右侧的表达式就不再求值。 - -(2)delete 运算符 - -```javascript -delete a?.b -// 等同于 -a == null ? undefined : delete a.b -``` - -上面代码中,如果`a`是`undefined`或`null`,会直接返回`undefined`,而不会进行`delete`运算。 - -(3)括号的影响 - -如果属性链有圆括号,链判断运算符对圆括号外部没有影响,只对圆括号内部有影响。 - -```javascript -(a?.b).c -// 等价于 -(a == null ? undefined : a.b).c -``` - -上面代码中,`?.`对圆括号外部没有影响,不管`a`对象是否存在,圆括号后面的`.c`总是会执行。 - -一般来说,使用`?.`运算符的场合,不应该使用圆括号。 - -(4)报错场合 - -以下写法是禁止的,会报错。 - -```javascript -// 构造函数 -new a?.() -new a?.b() - -// 链判断运算符的右侧有模板字符串 -a?.`{b}` -a?.b`{c}` - -// 链判断运算符的左侧是 super -super?.() -super?.foo - -// 链运算符用于赋值运算符左侧 -a?.b = c -``` - -(5)右侧不得为十进制数值 - -为了保证兼容以前的代码,允许`foo?.3:0`被解析成`foo ? .3 : 0`,因此规定如果`?.`后面紧跟一个十进制数字,那么`?.`不再被看成是一个完整的运算符,而会按照三元运算符进行处理,也就是说,那个小数点会归属于后面的十进制数字,形成一个小数。 - -## Null 判断运算符 - -读取对象属性的时候,如果某个属性的值是`null`或`undefined`,有时候需要为它们指定默认值。常见做法是通过`||`运算符指定默认值。 - -```javascript -const headerText = response.settings.headerText || 'Hello, world!'; -const animationDuration = response.settings.animationDuration || 300; -const showSplashScreen = response.settings.showSplashScreen || true; -``` - -上面的三行代码都通过`||`运算符指定默认值,但是这样写是错的。开发者的原意是,只要属性的值为`null`或`undefined`,默认值就会生效,但是属性的值如果为空字符串或`false`或`0`,默认值也会生效。 - -为了避免这种情况,[ES2020](https://github.com/tc39/proposal-nullish-coalescing) 引入了一个新的 Null 判断运算符`??`。它的行为类似`||`,但是只有运算符左侧的值为`null`或`undefined`时,才会返回右侧的值。 - -```javascript -const headerText = response.settings.headerText ?? 'Hello, world!'; -const animationDuration = response.settings.animationDuration ?? 300; -const showSplashScreen = response.settings.showSplashScreen ?? true; -``` - -上面代码中,默认值只有在左侧属性值为`null`或`undefined`时,才会生效。 - -这个运算符的一个目的,就是跟链判断运算符`?.`配合使用,为`null`或`undefined`的值设置默认值。 - -```javascript -const animationDuration = response.settings?.animationDuration ?? 300; -``` - -上面代码中,如果`response.settings`是`null`或`undefined`,或者`response.settings.animationDuration`是`null`或`undefined`,就会返回默认值300。也就是说,这一行代码包括了两级属性的判断。 - -这个运算符很适合判断函数参数是否赋值。 - -```javascript -function Component(props) { - const enable = props.enabled ?? true; - // … -} -``` - -上面代码判断`props`参数的`enabled`属性是否赋值,基本等同于下面的写法。 - -```javascript -function Component(props) { - const { - enabled: enable = true, - } = props; - // … -} -``` - -`??`有一个运算优先级问题,它与`&&`和`||`的优先级孰高孰低。现在的规则是,如果多个逻辑运算符一起使用,必须用括号表明优先级,否则会报错。 - -```javascript -// 报错 -lhs && middle ?? rhs -lhs ?? middle && rhs -lhs || middle ?? rhs -lhs ?? middle || rhs -``` - -上面四个表达式都会报错,必须加入表明优先级的括号。 - -```javascript -(lhs && middle) ?? rhs; -lhs && (middle ?? rhs); - -(lhs ?? middle) && rhs; -lhs ?? (middle && rhs); - -(lhs || middle) ?? rhs; -lhs || (middle ?? rhs); - -(lhs ?? middle) || rhs; -lhs ?? (middle || rhs); -``` - diff --git a/docs/operator.md b/docs/operator.md new file mode 100644 index 0000000..e3958b4 --- /dev/null +++ b/docs/operator.md @@ -0,0 +1,309 @@ +# 运算符的扩展 + +本章介绍 ES6 后续标准添加的一些运算符。 + +## 指数运算符 + +ES2016 新增了一个指数运算符(`**`)。 + +```javascript +2 ** 2 // 4 +2 ** 3 // 8 +``` + +这个运算符的一个特点是右结合,而不是常见的左结合。多个指数运算符连用时,是从最右边开始计算的。 + +```javascript +// 相当于 2 ** (3 ** 2) +2 ** 3 ** 2 +// 512 +``` + +上面代码中,首先计算的是第二个指数运算符,而不是第一个。 + +指数运算符可以与等号结合,形成一个新的赋值运算符(`**=`)。 + +```javascript +let a = 1.5; +a **= 2; +// 等同于 a = a * a; + +let b = 4; +b **= 3; +// 等同于 b = b * b * b; +``` + +## 链判断运算符 + +编程实务中,如果读取对象内部的某个属性,往往需要判断一下,属性的上层对象是否存在。比如,读取`message.body.user.firstName`这个属性,安全的写法是写成下面这样。 + +```javascript +// 错误的写法 +const firstName = message.body.user.firstName || 'default'; + +// 正确的写法 +const firstName = (message + && message.body + && message.body.user + && message.body.user.firstName) || 'default'; +``` + +上面例子中,`firstName`属性在对象的第四层,所以需要判断四次,每一层是否有值。 + +三元运算符`?:`也常用于判断对象是否存在。 + +```javascript +const fooInput = myForm.querySelector('input[name=foo]') +const fooValue = fooInput ? fooInput.value : undefined +``` + +上面例子中,必须先判断`fooInput`是否存在,才能读取`fooInput.value`。 + +这样的层层判断非常麻烦,因此 [ES2020](https://github.com/tc39/proposal-optional-chaining) 引入了“链判断运算符”(optional chaining operator)`?.`,简化上面的写法。 + +```javascript +const firstName = message?.body?.user?.firstName || 'default'; +const fooValue = myForm.querySelector('input[name=foo]')?.value +``` + +上面代码使用了`?.`运算符,直接在链式调用的时候判断,左侧的对象是否为`null`或`undefined`。如果是的,就不再往下运算,而是返回`undefined`。 + +下面是判断对象方法是否存在,如果存在就立即执行的例子。 + +```javascript +iterator.return?.() +``` + +上面代码中,`iterator.return`如果有定义,就会调用该方法,否则`iterator.return`直接返回`undefined`,不再执行`?.`后面的部分。 + +对于那些可能没有实现的方法,这个运算符尤其有用。 + +```javascript +if (myForm.checkValidity?.() === false) { + // 表单校验失败 + return; +} +``` + +上面代码中,老式浏览器的表单对象可能没有`checkValidity()`这个方法,这时`?.`运算符就会返回`undefined`,判断语句就变成了`undefined === false`,所以就会跳过下面的代码。 + +链判断运算符`?.`有三种写法。 + +- `obj?.prop` // 对象属性是否存在 +- `obj?.[expr]` // 同上 +- `func?.(...args)` // 函数或对象方法是否存在 + +下面是`obj?.[expr]`用法的一个例子。 + +```bash +let hex = "#C0FFEE".match(/#([A-Z]+)/i)?.[1]; +``` + +上面例子中,字符串的`match()`方法,如果没有发现匹配会返回`null`,如果发现匹配会返回一个数组,`?.`运算符起到了判断作用。 + +下面是`?.`运算符常见形式,以及不使用该运算符时的等价形式。 + +```javascript +a?.b +// 等同于 +a == null ? undefined : a.b + +a?.[x] +// 等同于 +a == null ? undefined : a[x] + +a?.b() +// 等同于 +a == null ? undefined : a.b() + +a?.() +// 等同于 +a == null ? undefined : a() +``` + +上面代码中,特别注意后两种形式,如果`a?.b()`和`a?.()`。如果`a?.b()`里面的`a.b`有值,但不是函数,不可调用,那么`a?.b()`是会报错的。`a?.()`也是如此,如果`a`不是`null`或`undefined`,但也不是函数,那么`a?.()`会报错。 + +使用这个运算符,有几个注意点。 + +(1)短路机制 + +本质上,`?.`运算符相当于一种短路机制,只要不满足条件,就不再往下执行。 + +```javascript +a?.[++x] +// 等同于 +a == null ? undefined : a[++x] +``` + +上面代码中,如果`a`是`undefined`或`null`,那么`x`不会进行递增运算。也就是说,链判断运算符一旦为真,右侧的表达式就不再求值。 + +(2)括号的影响 + +如果属性链有圆括号,链判断运算符对圆括号外部没有影响,只对圆括号内部有影响。 + +```javascript +(a?.b).c +// 等价于 +(a == null ? undefined : a.b).c +``` + +上面代码中,`?.`对圆括号外部没有影响,不管`a`对象是否存在,圆括号后面的`.c`总是会执行。 + +一般来说,使用`?.`运算符的场合,不应该使用圆括号。 + +(3)报错场合 + +以下写法是禁止的,会报错。 + +```javascript +// 构造函数 +new a?.() +new a?.b() + +// 链判断运算符的右侧有模板字符串 +a?.`{b}` +a?.b`{c}` + +// 链判断运算符的左侧是 super +super?.() +super?.foo + +// 链运算符用于赋值运算符左侧 +a?.b = c +``` + +(4)右侧不得为十进制数值 + +为了保证兼容以前的代码,允许`foo?.3:0`被解析成`foo ? .3 : 0`,因此规定如果`?.`后面紧跟一个十进制数字,那么`?.`不再被看成是一个完整的运算符,而会按照三元运算符进行处理,也就是说,那个小数点会归属于后面的十进制数字,形成一个小数。 + +## Null 判断运算符 + +读取对象属性的时候,如果某个属性的值是`null`或`undefined`,有时候需要为它们指定默认值。常见做法是通过`||`运算符指定默认值。 + +```javascript +const headerText = response.settings.headerText || 'Hello, world!'; +const animationDuration = response.settings.animationDuration || 300; +const showSplashScreen = response.settings.showSplashScreen || true; +``` + +上面的三行代码都通过`||`运算符指定默认值,但是这样写是错的。开发者的原意是,只要属性的值为`null`或`undefined`,默认值就会生效,但是属性的值如果为空字符串或`false`或`0`,默认值也会生效。 + +为了避免这种情况,[ES2020](https://github.com/tc39/proposal-nullish-coalescing) 引入了一个新的 Null 判断运算符`??`。它的行为类似`||`,但是只有运算符左侧的值为`null`或`undefined`时,才会返回右侧的值。 + +```javascript +const headerText = response.settings.headerText ?? 'Hello, world!'; +const animationDuration = response.settings.animationDuration ?? 300; +const showSplashScreen = response.settings.showSplashScreen ?? true; +``` + +上面代码中,默认值只有在左侧属性值为`null`或`undefined`时,才会生效。 + +这个运算符的一个目的,就是跟链判断运算符`?.`配合使用,为`null`或`undefined`的值设置默认值。 + +```javascript +const animationDuration = response.settings?.animationDuration ?? 300; +``` + +上面代码中,如果`response.settings`是`null`或`undefined`,或者`response.settings.animationDuration`是`null`或`undefined`,就会返回默认值300。也就是说,这一行代码包括了两级属性的判断。 + +这个运算符很适合判断函数参数是否赋值。 + +```javascript +function Component(props) { + const enable = props.enabled ?? true; + // … +} +``` + +上面代码判断`props`参数的`enabled`属性是否赋值,基本等同于下面的写法。 + +```javascript +function Component(props) { + const { + enabled: enable = true, + } = props; + // … +} +``` + +`??`本质上是逻辑运算,它与其他两个逻辑运算符`&&`和`||`有一个优先级问题,它们之间的优先级到底孰高孰低。优先级的不同,往往会导致逻辑运算的结果不同。 + +现在的规则是,如果多个逻辑运算符一起使用,必须用括号表明优先级,否则会报错。 + +```javascript +// 报错 +lhs && middle ?? rhs +lhs ?? middle && rhs +lhs || middle ?? rhs +lhs ?? middle || rhs +``` + +上面四个表达式都会报错,必须加入表明优先级的括号。 + +```javascript +(lhs && middle) ?? rhs; +lhs && (middle ?? rhs); + +(lhs ?? middle) && rhs; +lhs ?? (middle && rhs); + +(lhs || middle) ?? rhs; +lhs || (middle ?? rhs); + +(lhs ?? middle) || rhs; +lhs ?? (middle || rhs); +``` + +## 逻辑赋值运算符 + +ES2021 引入了三个新的[逻辑赋值运算符](https://github.com/tc39/proposal-logical-assignment)(logical assignment operators),将逻辑运算符与赋值运算符进行结合。 + +```javascript +// 或赋值运算符 +x ||= y +// 等同于 +x || (x = y) + +// 与赋值运算符 +x &&= y +// 等同于 +x && (x = y) + +// Null 赋值运算符 +x ??= y +// 等同于 +x ?? (x = y) +``` + +这三个运算符`||=`、`&&=`、`??=`相当于先进行逻辑运算,然后根据运算结果,再视情况进行赋值运算。 + +它们的一个用途是,为变量或属性设置默认值。 + +```javascript +// 老的写法 +user.id = user.id || 1; + +// 新的写法 +user.id ||= 1; +``` + +上面示例中,`user.id`属性如果不存在,则设为`1`,新的写法比老的写法更紧凑一些。 + +下面是另一个例子。 + +```javascript +function example(opts) { + opts.foo = opts.foo ?? 'bar'; + opts.baz ?? (opts.baz = 'qux'); +} +``` + +上面示例中,参数对象`opts`如果不存在属性`foo`和属性`bar`,则为这两个属性设置默认值。有了“Null 赋值运算符”以后,就可以统一写成下面这样。 + +```javascript +function example(opts) { + opts.foo ??= 'bar'; + opts.baz ??= 'qux'; +} +``` + diff --git a/docs/promise.md b/docs/promise.md index 02c1d06..ea5b047 100644 --- a/docs/promise.md +++ b/docs/promise.md @@ -758,9 +758,25 @@ try { ## Promise.any() -ES2021 引入了[`Promise.any()`方法](https://github.com/tc39/proposal-promise-any)。该方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例返回。只要参数实例有一个变成`fulfilled`状态,包装实例就会变成`fulfilled`状态;如果所有参数实例都变成`rejected`状态,包装实例就会变成`rejected`状态。 +ES2021 引入了[`Promise.any()`方法](https://github.com/tc39/proposal-promise-any)。该方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例返回。 -`Promise.any()`跟`Promise.race()`方法很像,只有一点不同,就是不会因为某个 Promise 变成`rejected`状态而结束。 +```javascript +Promise.any([ + fetch('https://v8.dev/').then(() => 'home'), + fetch('https://v8.dev/blog').then(() => 'blog'), + fetch('https://v8.dev/docs').then(() => 'docs') +]).then((first) => { // 只要有一个 fetch() 请求成功 + console.log(first); +}).catch((error) => { // 所有三个 fetch() 全部请求失败 + console.log(error); +}); +``` + +只要参数实例有一个变成`fulfilled`状态,包装实例就会变成`fulfilled`状态;如果所有参数实例都变成`rejected`状态,包装实例就会变成`rejected`状态。 + +`Promise.any()`跟`Promise.race()`方法很像,只有一点不同,就是`Promise.any()`不会因为某个 Promise 变成`rejected`状态而结束,必须等到所有参数 Promise 变成`rejected`状态才会结束。 + +下面是`Promise()`与`await`命令结合使用的例子。 ```javascript const promises = [ @@ -768,6 +784,7 @@ const promises = [ fetch('/endpoint-b').then(() => 'b'), fetch('/endpoint-c').then(() => 'c'), ]; + try { const first = await Promise.any(promises); console.log(first); @@ -778,30 +795,18 @@ try { 上面代码中,`Promise.any()`方法的参数数组包含三个 Promise 操作。其中只要有一个变成`fulfilled`,`Promise.any()`返回的 Promise 对象就变成`fulfilled`。如果所有三个操作都变成`rejected`,那么`await`命令就会抛出错误。 -`Promise.any()`抛出的错误,不是一个一般的错误,而是一个 AggregateError 实例。它相当于一个数组,每个成员对应一个被`rejected`的操作所抛出的错误。下面是 AggregateError 的实现示例。 +`Promise.any()`抛出的错误,不是一个一般的 Error 错误对象,而是一个 AggregateError 实例。它相当于一个数组,每个成员对应一个被`rejected`的操作所抛出的错误。下面是 AggregateError 的实现示例。 ```javascript -new AggregateError() extends Array -> AggregateError +// new AggregateError() extends Array const err = new AggregateError(); err.push(new Error("first error")); err.push(new Error("second error")); +// ... throw err; ``` -捕捉错误时,如果不用`try...catch`结构和 await 命令,可以像下面这样写。 - -```javascript -Promise.any(promises).then( - (first) => { - // Any of the promises was fulfilled. - }, - (error) => { - // All of the promises were rejected. - } -); -``` - 下面是一个例子。 ```javascript diff --git a/docs/proposals.md b/docs/proposals.md index 5439055..4fb6848 100644 --- a/docs/proposals.md +++ b/docs/proposals.md @@ -314,83 +314,6 @@ 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 -``` - ## Math.signbit() `Math.sign()`用来判断一个值的正负,但是如果参数是`-0`,它会返回`-0`。 diff --git a/docs/set-map.md b/docs/set-map.md index 0fcfde8..ac2ca3e 100644 --- a/docs/set-map.md +++ b/docs/set-map.md @@ -1140,3 +1140,151 @@ c.dec() 上面代码中,`Countdown`类的两个内部属性`_counter`和`_action`,是实例的弱引用,所以如果删除实例,它们也就随之消失,不会造成内存泄漏。 +## WeakRef + +WeakSet 和 WeakMap 是基于弱引用的数据结构,[ES2021](https://github.com/tc39/proposal-weakrefs) 更进一步,提供了 WeakRef 对象,用于直接创建对象的弱引用。 + +```javascript +let target = {}; +let wr = new WeakRef(target); +``` + +上面示例中,`target`是原始对象,构造函数`WeakRef()`创建了一个基于`target`的新对象`wr`。这里,`wr`就是一个 WeakRef 的示例,属于对`target`的弱引用,垃圾回收机制不会计入这个引用,也就是说,`wr`的引入不会妨碍原始对象`target`被垃圾回收机制清除。 + +WeakRef 实例对象有一个`deref()`方法,如果原始对象存在,该方法返回原始对象;如果原始对象已经被垃圾回收机制清除,该方法返回`undefined`。 + +```javascript +let target = {}; +let wr = new WeakRef(target); + +let obj = wr.deref(); +if (obj) { // target 未被垃圾回收机制清除 + // ... +} +``` + +上面示例中,`deref()`方法可以判断原始对象是否已被清除。 + +弱引用对象的一大用处,就是作为缓存,未被清除时可以从缓存取值,一旦清除缓存就自动失效。 + +```javascript +function makeWeakCached(f) { + const cache = new Map(); + return key => { + const ref = cache.get(key); + if (ref) { + const cached = ref.deref(); + if (cached !== undefined) return cached; + } + + const fresh = f(key); + cache.set(key, new WeakRef(fresh)); + return fresh; + }; +} + +const getImageCached = makeWeakCached(getImage); +``` + +上面示例中,`makeWeakCached()`用于建立一个缓存,缓存里面保存对原始文件的弱引用。 + +注意,标准规定,一旦使用`WeakRef()`创建了原始对象的弱引用,那么在本轮事件循环(event loop),原始对象肯定不会被清除,只会在后面的事件循环才会被清除。 + +## FinalizationRegistry + +[ES2021](https://github.com/tc39/proposal-weakrefs#finalizers) 引入了清理器注册表功能 FinalizationRegistry,用来指定目标对象被垃圾回收机制清除以后,所要执行的回调函数。 + +首先,新建一个注册表实例。 + +```javascript +const registry = new FinalizationRegistry(heldValue => { + // .... +}); +``` + +上面代码中,`FinalizationRegistry()`是系统提供的构造函数,返回一个清理器注册表实例,里面登记了所要执行的回调函数。回调函数作为`FinalizationRegistry()`的参数传入,它本身有一个参数`heldValue`。 + +然后,注册表实例的`register()`方法,用来注册所要观察的目标对象。 + +```javascript +registry.register(theObject, "some value"); +``` + +上面示例中,`theObject`就是所要观察的目标对象,一旦该对象被垃圾回收机制清除,注册表就会在清除完成后,调用早前注册的回调函数,并将`some value`作为参数(前面的`heldValue`)传入回调函数。 + +注意,注册表不对目标对象`theObject`构成强引用,属于弱引用。因为强引用的话,原始对象就不会被垃圾回收机制清除,这就失去使用注册表的意义了。 + +回调函数的参数`heldValue`可以是任意类型的值,字符串、数值、布尔值、对象,甚至可以是`undefined`。 + +最后,如果以后还想取消已经注册的回调函数,则要向`register()`传入第三个参数,作为标记值。这个标记值必须是对象,一般都用原始对象。接着,再使用注册表实例对象的`unregister()`方法取消注册。 + +```javascript +registry.register(theObject, "some value", theObject); +// ...其他操作... +registry.unregister(theObject); +``` + +上面代码中,`register()`方法的第三个参数就是标记值`theObject`。取消回调函数时,要使用`unregister()`方法,并将标记值作为该方法的参数。这里`register()`方法对第三个参数的引用,也属于弱引用。如果没有这个参数,则回调函数无法取消。 + +由于回调函数被调用以后,就不再存在于注册表之中了,所以执行`unregister()`应该是在回调函数还没被调用之前。 + +下面使用`FinalizationRegistry`,对前一节的缓存函数进行增强。 + +```javascript +function makeWeakCached(f) { + const cache = new Map(); + const cleanup = new FinalizationRegistry(key => { + const ref = cache.get(key); + if (ref && !ref.deref()) cache.delete(key); + }); + + return key => { + const ref = cache.get(key); + if (ref) { + const cached = ref.deref(); + if (cached !== undefined) return cached; + } + + const fresh = f(key); + cache.set(key, new WeakRef(fresh)); + cleanup.register(fresh, key); + return fresh; + }; +} + +const getImageCached = makeWeakCached(getImage); +``` + +上面示例与前一节的例子相比,就是增加一个清理器注册表,一旦缓存的原始对象被垃圾回收机制清除,会自动执行一个回调函数。该回调函数会清除缓存里面已经失效的键。 + +下面是另一个例子。 + +```javascript +class Thingy { + #file; + #cleanup = file => { + console.error( + `The \`release\` method was never called for the \`Thingy\` for the file "${file.name}"` + ); + }; + #registry = new FinalizationRegistry(this.#cleanup); + + constructor(filename) { + this.#file = File.open(filename); + this.#registry.register(this, this.#file, this.#file); + } + + release() { + if (this.#file) { + this.#registry.unregister(this.#file); + File.close(this.#file); + this.#file = null; + } + } +} +``` + +上面示例中,如果由于某种原因,`Thingy`类的实例对象没有调用`release()`方法,就被垃圾回收机制清除了,那么清理器就会调用回调函数`#cleanup()`,输出一条错误信息。 + +由于无法知道清理器何时会执行,所以最好避免使用它。另外,如果浏览器窗口关闭或者进程意外退出,清理器则不会运行。 +