ECMAScript 2020 简介

自 ECMAScript 2015 以来,ES 将不再进行大幅度变更,而是改为每年小副更新:每年某个时候, TC39 会整理并发布一份新版的草案,并在经过一段时间的增补修订之后将其提交给 ECMA 组织,开始正式生效。

因此从去年开始,我准备在每年年初时系统地了解一下新出的特性,并顺便写一篇文章。而这一篇,就是关于今年 ECMAScript 2020 的。(话说,最近三年每年都是新增 8 个提案,标准组织里是不是某个对 8 有特殊的癖好)

可选链操作符(?.

首先要聊的是一个非常好用的操作符,当我们想要访问某个对象中的属性或方法而又不确定这个属性或方法是否存在时,就可以用到它了。

比如下面这种情况:

const el = document.querySelector(".some-class")
const height = el.clientHeight

但是,我们可能无法确定页面中是否真的有一个类名为 some-class 的元素,因此在访问 clientHeight 之前最好是先判断一下:

const height = el ? el.clientHeight : undefined

但这种代码写起来会很麻烦对吧?那么使用「可选链操作符」 ,就可以将代码简化成如下形式:

const height = el?.clientHeight

访问属性

只要是需要获取某个对象中的属性,就都可以使用该语法:

a?.b
a?.[x]

在上面的代码中,如果 a 为 undefinednull,则表达式会立即返回 undefined,否则返回所访问属性的值。也就是说,它们与下面这段代码是等价的:

a == null ? undefined : a.b
a == null ? undefined : a[x]

调用方法

而且,在尝试调用某个方法时,也可以使用该语法:

a?.()

规则同样是如果 a 为 undefinednull,则返回 undefined,否则将调用该方法。不过需要额外注意的是,该操作符并不会判断 a 是否是函数类型,因此如果 a 是一个其它类型的值,那么这段代码依然会在运行时抛出异常。

访问深层次属性

另外,在访问某个对象较深层级的属性时,也可以串联使用该操作符:

a?.b?.[0]?.()?.d

不过可能有人会懒得先去判断是否真的有必要,就一股脑给访问链路中的每个属性都加上该操作符,毕竟写起来也不费事。

但就像上面代码中所展示的那样,这种代码真不怎么好看,可读性也很差。而且若真的有一个应当存在的对象因为某些 bug 导致它没有存在,那么在访问它时就应当是抛出异常,这样可以及时发现问题,而不是使它被隐藏起来。

所以总的来说,还是建议只在必要的时候才使用可选链操作符。毕竟语法糖这种东西,虽然可以帮助我们缓解手指的压力,但也会迷惑我们的大脑,使我们忽略掉一些细节,到时候,糖里面包着的可能就不是什么好东西了。

其它特性

除此之外,该操作符也支持短路特性:

a?.[++x]      // 当 a 为 `undefined` 或 `null` 时,x 不会递增
a == null ? undefined : a[++x]

以及可以很好地与 delete 运算符一起使用:

delete a?.b    // 当 a 为 `undefined` 或 `null` 时,delete 将直接返回 true
a == null ? true : delete a.b

最后,还需要注意的一点是括号,括号将会限定该运算符的作用范围:

(a.?b).c
(a == null ? undefined : a.b).c

这是符合括号的语义的,但会让代码变得有点晦涩,而且在该操作符的实际应用场景中,貌似也并不需要使用括号,因此了解一下该特性即可。

空值合并运算符(??

在使用 JS 的时候,我们经常会写出类似下面这样的代码:

const scrollTop = window.pageYOffset || document.documentElement.scrollTop

也就是在某个值不存在时改用另外一个值。

但这里我们使用的是 OR 运算符,该运算符的规则是:如果左操作数为 假值,则会返回右操作数。而有的时候,我们只想在左操作数为 undefinednull 时才返回右操作数,而其它假值比如 0 则应当是有效值,就像上面代码中的情况。

如果想要做到这一点,在这之前就只能使用三元运算符来实现:

const scrollTop = (window.pageYOffset !== undefined) ? window.pageYOffset : document.documentElement.scrollTop

但现在,我们可以使用空值合并运算符:

const scrollTop = window.pageYOffset ?? document.documentElement.scrollTop

这个运算符的规则正好是我们所想要的:如果左操作数为 undefinednull,则会返回右操作数:

const a = {
    b: undefined,
    c: null,
    d: 0,
    e: "",
    f: false,
};

a.b ?? "default"   // => "default"
a.c ?? "default"   // => "default"
a.d ?? 300         // => 0
a.e ?? "hello"     // => ""
a.f ?? true        // => false

而且该运算符也支持短路特性:

const x = a ?? getDefaultValue()
// 当 `a` 不为 `undefined` 或 `null` 时,`getDefaultValue` 方法不会被执行

但需要注意一点,该运算符不能与 AND 或 OR 运算符共用,否则会抛出语法异常:

a && b ?? "default"    // SyntaxError

因为这种代码的歧义比较严重,在不同人的理解中,可能有的人觉得按 (a && b) ?? "default" 运行是合理的,而另外一些人却觉得按 a && (b ?? "default") 运行才对,因此在设计该运算符时就干脆通过语法上的约束来避免了这种情况。如果确实需要在同一个表达式中同时使用它们,那么使用括号加以区分即可:

(a && b) ?? "default"

总的来说,这个操作符的主要设计目的是为了给可选链操作符提供一个补充运算符,因此通常是和可选链操作符一起使用的:

const x = a?.b ?? 0;

String.prototype.matchAll

原本字符串原型中已经有一个 match 方法,但这个方法的处理逻辑比较复杂,它的返回结果是一个数组,但该数组中的内容会根据所传入的正则表达式中是否有 g 标志而有所不同:

"test1test2".match(/test(\d)/)    // => ["test1", "1"]
"test1test2".match(/test(\d)/g)   // => ["test1", "test2"]

如果所传入的正则表达式中没有 g 标志,则会返回字符串中的第一个匹配项及其相关的捕获组(["test1", "1"],而若有 g 标志,则只会返回匹配的所有结果值,没有捕获组(["test1", "test2"])。

而若想要拿到所有匹配的结果值,以及每个结果值对应的捕获组等相关信息,那么只能自己实现,无论是循环调用 regex.exec,还是使用 string.replace,写起来都颇为麻烦。

而这个新的 matchAll 方法便可以很方便地解决这种问题,而且更灵活,也更强大。

返回数据

首先要注意的是,matchAll 方法的返回值是一个迭代器:

const matchs = "test1test2".matchAll(/test(\d)/g)

matchs.next()  // => { value: ["test1", "1"], done: false }
matchs.next()  // => { value: ["test2", "2"], done: false }
matchs.next()  // => { value: undefined, done: true }

每当迭代器的 next 方法被执行时,其内部都会进行一次正则匹配并返回匹配项及其相关的捕获组。而当下一次 next 被执行时,将从上一次匹配结果在字符串中的索引位置之后开始继续匹配。当所有匹配结果都返回后,再次调用 next 方法将返回 undefined

如果希望一次性拿到所有匹配结果数据,那么使用展开语法或 Array.from 即可:

const string = "test1test2"
const regexp = /test(\d)/g

const matchs = [...string.matchAll(regexp)]
// or
const matchs = Array.from(string.matchAll(regexp))

// matchs => [
//     ["test1", "1"],
//     ["test2", "2"],
// ]

参数

另外,大家可能已经注意我们的正则表达式声明使用了 g 标志,只有所传入的正则中有该标志,迭代器才会匹配字符串中的所有匹配结果,否则在执行完第一次匹配后便会结束:

const matchs = "test1test2".matchAll(/test(\d)/)

matchs.next()  // => { value: ["test1", "1"], done: false }
matchs.next()  // => { value: undefined, done: true }

而如果传给 matchAll 的参数值不是一个「 正则表达式」 对象,那么 matchAll 内部隐式执行 new RegExp(arg, 'g') 以将其转换为一个正则对象:

const matchs = "test1test2".matchAll("test(\\d)")

matchs.next()  // => { value: ["test1", "1"], done: false }
matchs.next()  // => { value: ["test2", "2"], done: false }
matchs.next()  // => { value: undefined, done: true }

RegExp.prototype[@@matchAll]

最后还要强调的一点,是此次规范中并不只有 String.prototype.matchAll 这一个方法,配套地还有一个 RegExp.prototype[@@matchAll] 方法,因此我们可以实现一个自己的正则子类,并重写 matchAll 的逻辑(比如直接返回一个数组):

class CustomRegExp extends RegExp {
    [Symbol.matchAll](string) {
        const matchs = super[Symbol.matchAll](string)
        return Array.from(matchs)
    }
}

const matchs = "test1test2".matchAll(new CustomRegExp("test(\\d)", "g"))

// matchs => [
//     ["test1", "1"],
//     ["test2", "2"],
// ]

甚至是任何类型的对象,只要它上面有实现 @@matchAll 方法,便可以作为匹配器传递给 matchAll 方法,当然,该特性的应用场景不多,大家了解就好。

import()

之前在 ES 2015 定义的模块语法中,所有模块导入语法都是静态声明的:

import defaultExport from "./module"
import * as exportName from "./module"
import { export1, export2 as alias2 } from "./module"
import "./module"

虽然这套语法已经足以满足绝大多数的导入需求,而且还可以支持实现静态分析以及树抖动等一系列重要的功能。但却无法满足一些需要动态导入的需求。

比如需要根据浏览器兼容性有选择地加载一些支持库,或是在实际需要时才加载某个模块的代码,再或者只是单纯地希望延迟加载某些模块来以渐进渲染的方式改进加载体验,等等这些,这在实际工作中也算是相当常见的一类需求了。

若没有动态导入,我们将难以实现这些需求。当然我们可以通过创建 script 标签来动态地导入某些脚本,但这是特定于浏览器环境的实现方式,也无法直接和现有的模块语法结合在一起使用,所以只能作为内部实现机制,但不能直接暴露给模块的使用者。

而使用 import() 则可以很好地处理这些需求。

简介

import() 是一个类似函数的语法关键字(就像 super() ),它接收一个字符串作为模块标识符,并返回一个 promise:

import('lodash').then(_ => {
    // Do something with lodash ...
})

import() 可以在任何支持该语法的平台中使用,比如 webpack、node 或 浏览器环境。而模块标识符的格式则是由各平台自行指定,比如 webpack 及 node 支持使用模块名直接加载 node_modules 中的模块,而浏览器支持使用 url 加载远程模块。

当模块及其所依赖的其它模块都被加载并执行完毕后,promise 将进入 fulfilled 状态,结果值便是包含该模块所有导出内容的一个对象:具名导出项被放在该对象的同名属性中,而默认导出项则放在名为 default 的属性中,比如有如下模块 a

export default 'hello';
export const x = 1;
export const y = 2;

那么导入结果便是:

import('a').then(module => {
    console.info(module)

    // => {
    //     default: 'hello',
    //     x: 1,
    //     y: 2,
    // }
});

当然如果因为模块不存在或无法访问等问题导致模块加载或执行失败,promise 便会进入 rejected 状态,你可以在其中执行一些回退处理。

历史

早期使用过 webpack 的人应当知道,webpack 早在 1.0 版本的时候就已经提供了对这类需求的支持,也就是 require.ensure

var a = require('normal-dep')

if ( module.hot ) {
  require.ensure(['b'], function(require) {
    var c = require('c')

    // Do something special...
  })
}

当然,这并非标准规范,而仅是 webpack 自己的特定实现方案。而且在 ES2015 推出之后,社区开始转向 ES 的模块语法,webpack 1.0 的那一套基于 CommonJS 语法的模块解析方案就显得有些过时了。

但在当时的 ES2015 中只有静态声明语法,因此无法支持动态导入的需求。但好在 ES2015 推出之后没有多久,2016 年的时候就由 Domenic Denicola 带头发起了有关于动态导入的讨论,并提交了相应的 规范草案,这就是我们今天看到的 import() 语法。

紧接着,webpack 便发布了支持 ES 模块语法的 2.0 版本,虽然 import() 的草案才刚提交没多久,离正式发布还有很长时间,但关于语法层面的讨论已经基本稳定,而且也已经满足 wbepack 的需求,因此 webpack 便采用了 import() 作为自己新版本中动态导入的语法方案。(而且凭借 webpack 那庞大的用户体量,如果 webpack 开始支持这一语法,并引导其用户全都使用它,那么这一语法自然会成为事实上的标准,而这将极大地反推标准组织采纳其相关的规范):

if ( module.hot ) {
  import('lodash').then(_ => {    
    // Do something with lodash ...
  })
}

再之后,其它库也逐渐开始支持 import() 语法,比如 React 的组件懒加载机制:

const OtherComponent = React.lazy(() => import('./OtherComponent'))

function MyComponent() {
    return (
        <div>
            <Suspense fallback={<div>Loading...</div>}>
                <OtherComponent />
            </Suspense>
        </div>
    )
}

这几年来,import() 的语法层面基本就没改动过,非常稳定,而底层也更多地只是细节上的完善。所以在 2019 年的时候,这一语法被正式纳入标准。

BigInt

在 ES 中,所有 Number 类型的值都使用 64 位浮点数格式存储,因此 Number 类型可以有效表示的最大整数为 2^53。

而使用新的 BigInt 类型,我们可以操作任意精度的整数。

用途

关于 BigInt,首先第一个问题就是「我们需要用到它吗」 ?那么让我们好好思考一下这个问题。

首先,第一个想到的就是 id,通常服务端都会为在数据库中存储的每条数据分配一个唯一 ID,而且通常这个 ID 都是一个从 1 开始递增的无符号 64 位整数;如果前后端需要相互传递这个 ID,那么在前端使用 Number 类型通常是有风险的,你公司的使用量越高,这个风险也超高,相应可能带来的损失也会超高。所以为了避免这个问题,我们前端通常都会要求服务端将 ID 转换为字符串类型。但毕竟是前端的锅,不应该让服务端来背,所以有了 BigInt 类型之后,也许这些风险问题就可以在前端处理掉了。

沿着这个思路,我们还能找到很多其它的数字,比如GUID、IPv6 地址、高精度时间戳、信用卡号、手机号等等。

另外,在数字计算或其它科学计算场景中,也应当有很多需要高精度数字的场景,最简单的例子,我们刚学编程时就学过的计算斐波那契数列和质数(唔,其它的我也不懂不知道)。

还有,看过《 编程珠玑》的应该知道其第一章中用到了位图来处理在有限内存空间中仅用一趟读写完成对一个文本文件内所存储的大量有限随机且不重复的正整数进行排序的问题(其实就是桶排序)。但书中的位图是使用字符串表示的,如果使用 BigInt 这种数据结构,那么就可以使用更少的内存空间完成更大范围内正整数的排序。

语法

在数字字面量的后面添加后缀 n 可以将其声明为 BigInt 类型:

const bigInt = 9007199254740993n

或者,使用其构造函数 BigInt

const bigInt = BigInt(9007199254740992)

// 当然,在超过 Number 最大整数限制时,我们也可以改为传入一个可能被正确解析的字符串
const bigInt = BigInt('9007199254740993')

运算

和 Number 类似,BigInt 也支持 +-***% 运算符:

3n + 2n    // => 5n
3n * 2n    // => 6n
3n ** 2n   // => 9n
3n % 2n    // => 1n

但不同的是,因为 BigInt 是纯粹的整数类型,无法表示小数位,因此 BigInt 的除法运算(/)的结果值依然还是一个整数(和 C/C++/Java 一样,表现为向下取整):

const bigInt = 3n;

bigInt / 2n;    // => 1n,而不是 1.5n

当然,位运算符也是支持的,除了无符号右移运算符:

1n & 3n    // => 1n
1n | 3n    // => 3n
1n ^ 3n    // => 2n
~1n        // => -2n
1n << 3n   // => 8n
1n >> 3n   // => 0n

1n >>> 3n  // Uncaught TypeError: BigInts have no unsigned right shift, use >> instead

和 Number 的关系

首先,BigInt 无法和 Number 一起运算,会抛出类型异常:

1n + 1
// Uncaught TypeError: Cannot mix BigInt and other types, use explicit conversions

一些内置模块如 Math 也不支持 BigInt,同样会抛出异常:

Math.pow(2n, 64n)
// Uncaught TypeError: Cannot convert a BigInt value to a number

另外,BigInt 和 Number 相等,但并不严格相等,因此:

1n == 1    // => true
1n === 1   // => false

但还好他们之间还是可以比较大小的:

1n < 2     // => true
1n < 1     // => false

2n > 1     // => true
2n > 2     // => false

而且在转换为 Boolean 值时,也和 Number 一样,0n 转为 false,其它值转为 true

!!0n       // => false
!!1n       // => true

另外两者之间只能使用对方的构造函数进行转换:

Number(1n) // => 1
BigInt(1)  // => 1n

但两者之间的转换也都有一些边界问题:

// 当 BigInt 值的精度超出 Number 类型可表示的范围时,会出现精度丢失的问题
Number(9007199254740993n)
// => 9007199254740992

// 当 Number 值中有小数位时,BigInt 会抛出异常
BigInt(1.1)
// VM4854:1 Uncaught RangeError: The number 1.1 cannot be converted to a BigInt because it is not an integer

联接字符串

还好的是,BigInt 可以和字符串之间使用 + 运算符连接:

1n + ' Number'   // => 1 Number
'Number ' + 2n   // => Number 2

TypedArray

配套地,在类型化数组中也提供了与 BigInt 对应的两个数组类型:BigInt64ArrayBigUint64Array

const array = new BigInt64Array(4);

array[0]   // => 0n

array[0] = 1n
array[0]   // => 1n

但因为每个元素限定只有 64 位,因此即便使用无符号类型,最大也只能表示 2^64 - 1:

const array = new BigUint64Array(4);

array[0] = 2n ** 64n
array[0]   // => 0n

array[0] = 2n ** 64n - 1n
array[0]   // => 18446744073709551615n

Promise.allSettled

Promise 上有提供一组组合方法(比如最常用到的 Promise.all),它们都是接收多个 promise 对象,并返回一个表示组合结果的新的 promise,依据所传入 promise 的结果状态,组合后的 promise 将切换为不同的状态。算上尚未最终通过的提案,目前为止这类方法一共有如下四个:

  • Promise.all 返回一个组合后的 promise,当所有 promise 全部切换为 fulfilled 状态后,该 promise 切换为 fulfilled 状态;但若有任意一个 promise 切换为 rejected 状态,该 promise 将立即切换为 rejected 状态;
  • Promise.race 返回一个组合后的 promise,当 promise 中有任意一个切换为 fulfilledrejected 状态时,该 promise 将立即切换为相同状态;
  • Promise.allSettled 返回一个组合后的 promise,当所有 promise 全部切换为 fulfilledrejected 状态时,该 promise 将切换为 fulfilled 状态;
  • Promise.any 返回一个组合后的 promise,当 promise 中有任意一个切换为 fulfilled 状态时,该 promise 将立即切换为 fulfilled 状态,但只有所有 promise 全部切换为 rejected 状态时,该 promise 才切换为 rejected 状态。(提案中)

这四个方法之间仅有判断逻辑上的区别,也都有各自所适用的场景,而今天,我们就主要聊一下 Promise.allSettled

用法

大致的用法上面已经说过了,而且也非常简单,无非就是传入一个数组,里面放任意多个 promise 对象,并接受一个表示组合结果的新的 promise。

需要注意的是,组合后的 promise 会等待所有所传入的 promise,当它们全部切换状态后(无论是 fulfilled 状态 还是 rejected 状态),这个组合后的 promise 会切换到 fulfilled 状态并给出所有 promise 的结果信息:

async function a() {
    const promiseA = fetch('/api/a')    // => fulfilled, "a"
    const promiseB = fetch('/api/b')    // => fulfilled, "b"
    const promiseC = fetch('/api/c')    // => rejected,  <Error: c>

    const results = await Promise.allSettled([ promiseA, promiseB, promiseC ])

	results.length   // => 3
    results[0]       // => { status: "fulfilled", value: "a" }
    results[1]       // => { status: "fulfilled", value: "b" }
    results[2]       // => { status: "rejected", reason: <Error: c> }
}

因为结果值是一个数组,所以你可以很容易地过滤出任何你感兴趣的结果信息:

// 获取所有 fulfilled 状态的结果信息
results.filter( result => result.status === "fulfilled" )

// 获取所有 rejected 状态的结果信息
results.filter( result => result.status === "rejected" )

// 获取第一个 rejected 状态的结果信息
results.find( result => result.status === "rejected" )

使用场景

首先,第一个容易想到的使用场景是在页面初始化的时候:有时候我们在进行一个页面的初始化流程时,需要加载多份初始化数据,或执行一些其它初始化操作,而且通常会希望等待这些初始化操作全部完成之后再执行后续流程:

async function init() {
    setInited(false)
    setInitError(undefined)

    const results = await Promise.allSettled([
        loadDetail(),
        loadRecommentListFirstPage(),
        initSDK(),
    ])

    const errors = results
        .filter( result => result.status === "rejected" )
        .map( rejectedResult => rejectedResult.reason )

    if (errors.length) {
        setInitError(errors[0])
        $logs.error(errors)
    }

    setInited(true)
}

或者,如果我们有自定义的全局消息中心,那么还可以基于 allSettled 作一些异步支持的事情。比如在打开登录弹出层并在用户成功登录后,向页面中广播一个 login 事件,通常页面中其它地方监听到该事件后需要向服务端请求新的数据,此时我们可能需要等待数据全部更新完毕之后再关闭登录弹出层:

async function login() {
    // goto login ...

    const results = messageCenter.login.emit()
    const promiseResults = results.filter(isPromise)

	if (promiseResults.length) {
        await Promise.allSettled(promiseResults)
	}

    closeLoginModal()
    closeLoading()
}

当然,肯定还有很多其它的用途,大家在工作中用心寻找即可。

globalThis

在浏览器环境中,我们可以有多种方式访问到全局对象,最常用到的肯定是 window,但除此之外还有 self,以及在特殊场景下使用的 framesparaent 以及 top

我们通常不怎么需要关心 windowself 之间的区别,但如果使用 Web Worker,那就应当了解 window 是只在主线程中才有的全局属性,在 Worker 线程中,我们需要改为使用 self

而在 node.js 环境中,我们需要使用 global,至于像 JSC.js 这种更小众的环境中,则需要使用 this ……

在一般的开发工作中,我们可能很少需要访问全局环境,而且大多时候也只需要基于一种环境进行开发,所以不太需要处理这种麻烦的问题。但是对于 es6-shim 这种需要支持多种环境的基础库来说,它们需要解决这个问题。

早先,我们可以通过下面这段代码较为方便地拿到全局对象:

var globals = (new Function('return this;'))()

但受到 Chrome APP 内容安全策略的影响(为缓解跨站脚本攻击的问题,该政策要求禁止使用 eval 及相关的功能),上面这段代码将无法在 Chrome APP 的运行环境中正常执行。

无奈之下,像 es6-shim 这种库就只能穷举所有可能的全局属性

var getGlobal = function () {
    // the only reliable means to get the global object is
    // `Function('return this')()`
    // However, this causes CSP violations in Chrome apps.
    if (typeof self !== 'undefined') { return self; }
    if (typeof window !== 'undefined') { return window; }
    if (typeof global !== 'undefined') { return global; }
    throw new Error('unable to locate global object');
};

var globals = getGlobal();

if (!globals.Reflect) {
    defineProperty(globals, 'Reflect', {}, true);
}

这种问题等真的遇到了,每次处理起来也是很麻烦的。所以才有了这次提案中的 globalThis

通过 globalThis,我们终于可以使用一种标准的方法拿到全局对象,而不用关心代码的运行环境。对于 es6-shim 这种库来说,这是一个极大的便利特性:

if (!globalThis.Reflect) {
    defineProperty(globalThis, 'Reflect', {}, true);
}

另外,关于 globalThis 还有一些细节的问题,比如为满足 Secure ECMAScript 的要求,globalThis 是可写的。而在浏览器页面中,受到 outer window 特性的影响,globalThis 实际指向的是 WindowProxy,而不是当前页面内真实的全局对象(该对象不能被直接访问)。

但这些细节上的问题通常很少会遇到,而且无须处理甚至也不应该处理,所以我们只需知道这些问题的存在即可。

for-in 结构

这最后一个进入标准的提案是用于规范 for-in 语句的遍历顺序的。

关于这个提案,我们首先要知道的一点是在之前的 ES 规范中几乎没有指定 for-in 语句在遍历时的顺序,但各 ES 引擎的实现在大多数情况下都是趋于一致的,只有在一些边界情况时才会有所差别。我们很难能够要求各引擎做到完全一致,主要原因在于 for-in 是 ES 中所有遍历 API 中最复杂的一个,再加上规范的疏漏,导致各大浏览器在实现该 API 时都有很多自己特有的实现逻辑,各引擎的维护人员很难有意愿去重新审查这部分的代码。

因此规范的作者作了大量的工作,去测试了很多现有的 ES 引擎中 for-in 的遍历逻辑。并梳理出了它们之间一致的部分,然后将这部分补充到了 ES 规范 当中。

另外,规范中还提供了一份示例代码,以供各引擎在实现 for-in 逻辑时参考使用,大家可以看一下:

function* EnumerateObjectProperties(obj) {
    const visited = new Set();

    for (const key of Reflect.ownKeys(obj)) {
        if (typeof key === "symbol") continue;
        const desc = Reflect.getOwnPropertyDescriptor(obj, key);
        if (desc) {
            visited.add(key);
            if (desc.enumerable) yield key;
        }
    }

    const proto = Reflect.getPrototypeOf(obj);
    if (proto === null) return;

    for (const protoKey of EnumerateObjectProperties(proto)) {
        if (!visited.has(protoKey)) yield protoKey;
    }
}

结语

最近的业余时间几乎都用在了解 ES 2020 中新的特性并总结这篇文章上了,每天晚上一个章节,前后花了一个多星期的时间才搞完。

想着今年要写的细致一点,导致这篇文章比较长,有 6400 多字,光是读完几乎都要用上小半个小时。但即便如此,也还是有很多点没有聊到。算了,就这样吧,懒得回头补充了,累死了都要。

而且受疫情的影响,公司也乱七八糟的,等这阵过去,还要考虑工作上的事。

总之,今年就是这样了。