这次的 TC39 会议只有两天的时间,如 JavaScript 运算符重载与十进制计算、Wasm-JS 互操作性等议题都延迟到了 10 月份讨论。即使如此,这次会议还是有许多对于开发者来说非常有帮助的议题如 Pipeline 运算符、JavaScript 固定布局对象等有了阶段性进展。
Stage 3 → Stage 4
从 Stage 3 进入到 Stage 4 有以下几个门槛:
  1. 必须编写与所有提案内容对应的 tc39/test262 测试,用于给各大 JavaScript 引擎和 transpiler 等实现检查与标准的兼容程度,并且 test262 已经合入了提案所需要的测试用例;
  2. 至少要有两个实现能够兼容上述 Test 262 测试,并发布到正式版本中;
  1. 发起了将提案内容合入正式标准文本 tc39/ecma262 的 Pull Request,并被 ECMAScript 编辑签署同意意见。

Relative indexing .at() method

提案链接:https://github.com/tc39/proposal-relative-indexing-method
很多时候,类似于 Python 中的数组负值索引可以非常实用。比如在 Python 中我们可以通过 arr[-1] 来访问数组中的最后一个元素,而不用通过目前 JavaScript 中的方式来访问 arr[arr.length-1]。这里的负数是作为从起始元素(即arr[0])开始的反向索引。
但是现在 JavaScript 中的问题是,[] 这个语法不仅仅只是在数组中使用(当然在 Python 中也不是),而在数组中也不仅仅只可以作为索引使用。像arr[1]一样通过索引引用一个值,事实上引用的是这个对象的 "1" 这个属性。所以 arr[-1] 已经完全可以在现在的 JavaScript 引擎中使用,只是它可能不是代表的我们想要表达的意思而已:它引用的是目标对象的 "-1" 这个属性,而不是一个反向索引。
这个提案提供了一个通用的方案,我们可以通过任意可索引的类型(Array,String,和 TypedArray)上的 .at 方法,来访问任意一个反向索引、或者是正向索引的元素。
// 数组[0, 1, 2, 3].at(-1); // => 3// 字符串'0123'.at(-1); // => '3'
这个特性已经在 Chrome 92、Firefox 90 中默认启用。

Object.hasOwn

提案链接:https://github.com/tc39/proposal-accessible-object-hasownproperty
现在我们就可以通过 Object.prototype.hasOwnProperty 来使用提案所包含的特性。但是直接通过对象自身的 hasOwnProperty 来使用 obj.hasOwnProperty('foo') 是不安全的,因为这个 obj 可能覆盖了 hasOwnProperty 的定义,MDN 上也对这种使用方式进行了警告
JavaScript 并没有保护 hasOwnProperty 这个属性名,因此,当某个对象可能自有一个占用该属性名的属性时,就需要使用外部的 hasOwnProperty 获得正确的结果...
Object.create(null).hasOwnProperty("foo")// Uncaught TypeError: Object.create(...).hasOwnProperty is not a functionlet object = { hasOwnProperty() {thrownewError("gotcha!") }}object.hasOwnProperty("foo")// Uncaught Error: gotcha!
所以一个正确的方式就得写成这样繁琐的方式:
let hasOwnProperty = Object.prototype.hasOwnPropertyif (hasOwnProperty.call(object, "foo")) {console.log("has property foo")}
而提案在 Object 上增加了一个 hasOwn 方法,便于大部分场景使用:
let object = { foo: false }Object.hasOwn(object, "foo") // truelet object2 = Object.create({ foo: true })Object.hasOwn(object2, "foo") // falselet object3 = Object.create(null)Object.hasOwn(object3, "foo") // false
这个特性将在 Chrome 93、Firefox 91 中默认启用。

Class static initialization blocks

提案链接:https://github.com/tc39/proposal-class-static-block
自从有了 Class Private Fields,对于类的语法是不断地有新的实践与需求。这个提案提议的 Class Static 初始化块会在类被执行、初始化时被执行。Java 等语言中也有类似的静态初始化代码块的能力,Static Initialization Blocks (https://docs.oracle.com/javase/tutorial/java/javaOO/initial.html)
提案中定义的初始化代码块可以获得 class 内的作用域,如同 class 的方法一样,也意味着可以访问类的 #字段。通过这个定义,我们就可以实现 JavaScript 中的 Friend 类了。
let getX;exportclass C { #xconstructor(x) {this.#x = { data: x }; }static {// getX has privileged access to #x getX = (obj) => obj.#x; }}exportfunctionreadXData(obj) {return getX(obj).data;}
这个特性将在 Firefox 93、Chrome 94 中默认启用。
Stage 1 → Stage 2
从 Stage 1 进入到 Stage 2 需要完成撰写包含提案所有内容的标准文本的初稿。

Change Array by Copy

提案链接:https://github.com/tc39/proposal-change-array-by-copy
Array.prototype 上有非常多十分实用的方法,如 Array.prototype.popArray.prototype.sortArray.prototype.reverse 等等,这些方法通常都是直接就地修改当前的数组对象与其中的元素内容。如果我们需要避免修改原有的数组对象的话,通常我们可以通过 [...arr] 来快速浅拷贝一个数组对象,然后再对这个数组对象调用刚才所说的方法。
这在 Tuple 与 Record 类型正式引入 ECMAScript 之前确实没什么问题。但是如果我们需要引入 Tuple (一种内容不可变的数组),同时 Tuple 想要同样具备 Array.prototype 上的这些便捷方法的话,先有的 Array.prototype 上的就地修改的方法就不再兼容 Tuple 了,而 Tuple 也不能重用这些方法名来使用非就地修改的语义。所以这个提案就准备先为 JavaScript 引入多个就地修改方法的拷贝版,后续 Tuple 也就可以只支持这些拷贝版本的方法。
let arr = [ 3, 2, 1 ];arr.sort();arr; // => [ 1, 2, 3 ] 被修改了arr = [ 3, 2, 1 ];let sorted = arr.withSorted();sorted; // => [ 1, 2, 3 ]arr; // => [ 3, 2, 1 ] 没有被修改

Pipeline operator

提案链接:https://github.com/tc39/proposal-pipeline-operator
如果我们用了比如 lodash 等常见的函数库,我们会发现如果需要连续调用多次 lodash 函数,我们需要不断地包裹调用:
foo(bar(1, baz(x)[0]).method())
又或者是连续调用异步函数:
let text = await (await fetch(...)).text();
我们可以发现这些写法都不易读。实际使用中,读的顺序与执行的顺序也可能是相反的,造成理解上的困难。
而 Pipeline 运算符引入了调用返回值传播语法,让我们可以非常方便地书写类似的链式调用:
// 嵌套式调用foo(bar(1, baz(x)[0]).method())// Pipeline 运算符x |> baz(%)[0] |> bar(1,%).method() |> foo(%)// 嵌套 await 调用let text = await (await fetch(...)).text();// Pipeline 运算符let text = await fetch() |> await %.text();
目前的 Pipeline 运算符右手操作符可以是任意表达式加上一个 % 占位符代表传播的值,我们可以通过下表来看目前 Pipeline 运算符支持的表达形式:
传统写法
Pipeline 运算符
o.m(x)
x |> o.m(%)
o.m(0, x)
x |> o.m(0, %)
new o.m(x)
x |> new o.m(%)
o[x]
x |> o[%]
x[i]
x |> %[i]
x + 1
x |> % + 1
[0, x]
x |> [0, %]
{ key: x }
x |> { key: % }
await o.m(x)
x |> await o.m(%)
yield o.m(x)
x |> (yield o.m(%))
Stage 0 → Stage 1
从 Stage 0 进入到 Stage 1 有以下门槛:
  1. 找到一个 TC39 成员作为 champion 负责这个提案的演进;
  2. 明确提案需要解决的问题与需求和大致的解决方案;
  1. 有问题、解决方案的例子;
  2. 对 API 形式、关键算法、语义、实现风险等有讨论、分析。

    Stage 1 的提案会有可预见的比较大的改动,以下列出的例子并不代表提案最终会是例子中的语法、语义。

String.isUSVString

提案链接:https://github.com/guybedford/proposal-is-usv-string
JavaScript 字符串都是 UTF-16 编码的字符串。在 Web API 中,我们可以发现有些 API (如 URL、URLSearchParams 等等系列 API)都声明了需要 USVString 作为参数。什么是 USVString?USV 代表 Unicode Scalar Value,即 Unicode 标量值。根据 Unicode 定义,Unicode 的码位(Code Point)可以分成几个类别,分别是图形码(Graphic),格式码(Format),控制码(Control),私有码(Private-Use),代理码(Surrogate),非字符码(Noncharacter),与保留码(Reserved)。而其中的代理码又分成了高位代理码与低位代码码,只有当一个高位代码码与一个低位代理码组合成一个代理码对,才是一个合法的 Unicode 字符。
目前,JavaScript 字符串并不限制这个字符串的值是否是合法的 Unicode 值,比如我们可以编码一个字符串只有高位代理码,而没有低位代理码等等。而如严格的 Web URL API 定义必须要求参数字符串是合法的 Unicode 标量值,因此我们需要有方法能够去区分一个字符串是否是合法的 Unicode 标量值。
这个提案提出给 String 增加一个静态方法 String.isUSVString 来供开发者用来校验字符串是否是合法 Unicode 标量值。
String.isUSVString('\ud800'); // => falseString.isUSVString('\ud800\udc00'); // => true

Array.fromAsync

提案链接:https://github.com/tc39/proposal-array-from-async
目前,我们可以通过 Array.from(iterator) 来方便地将一个迭代器转换成一个数组来做后续的操作,比如:
function* f () {for (let i = 0; i < 4; i++)yield i;}Array.from(f()); => [0, 1, 2, 3].
随着 Async Generator 在 Web API 与 Node.js API 中应用的范围越来越广,如 Node.js 的 Streams API 也已经实现了 Symbol.asyncIterator 协议,或者是 Web 的 ReadableStream 等等。现在我们可以通过下面的方式便捷地迭代这些 stream 对象,比如文件流:
const fs = require('fs');asyncfunctionprint(readable) {// 设置编码格式为 UTF8 readable.setEncoding('utf8');// 异步迭代 readable streamlet data = '';forawait (const chunk of readable) { data += chunk; }// 输出console.log(data);}print(fs.createReadStream('file')).catch(console.error);
提案期望提供一个如同 Array.from 的内置 Array.asyncFrom 方法,用于从一个异步迭代器从组装出完整的数组,可以便捷地用于开发、调试等等用途:
const fs = require('fs');asyncfunctionprint(readable) {// 设置编码格式为 UTF8 readable.setEncoding('utf8');// 异步迭代 readable streamconst data = (awaitArray.asyncFrom(readable)).join('');// 输出console.log(data);}print(fs.createReadStream('file')).catch(console.error);

BigInt Math

提案链接:https://github.com/tc39/proposal-bigint-math
BigInt 在 JavaScript 中提供了任意精度的整型数值计算的能力。不过,目前 JavaScript 中对于 Number 类型的常见数学计算操作如 Math.maxMath.powMath 命名空间下的函数都没有对于 BigInt 的支持。目前如 Web Performance API、Node.js Performance API 都已经提供了基于 BigInt 的 API 实现,对于开发者来说这些常见的数学计算操作支持还是非常有必要性的。
提案提出了将 Math 命名空间下对于 BigInt 来说合理的函数增加 BigInt 支持,如 Math.maxMath.pow 等。而提案希望不包含对于 BigInt 可能造成精度丢失的函数如三角函数(Math.sin 等)、对数函数(Math.log 等)等等函数的支持,避免开发者误用。

Get Intrinsic

提案链接:https://github.com/ljharb/proposal-get-intrinsic
在 JavaScript 中,我们可以对任意对象做任意的操作,比如删除属性、替换方法、增加实现等等,比如我们常见的 Polyfill 或者 Monkey Patch 等方案的基础。而这同样也造成了一定问题,比如我们引入了一个第三方库,而这个第三方库对于固有对象如 Array.prototype 上的方法做了修改,如果这个修改的健壮性与兼容性有问题的话,对于其他的第三方库、应用代码都会造成不可预期的影响,如两个 Polyfill 库互相冲突等,甚至会破坏运行时的内置行为。比如 Node.js 的内置库如 fs 等等或者 Deno 的内置库都是使用 JavaScript 实现的,也就意味着他们都依赖于这些 ArrayMap 等等 JavaScript 固有对象实现,对这些对象的修改同样可能破坏 Node.js 与 Deno 的行为。因为,Node.js 等运行时或者 Polyfill 库为了避免被这些问题影响,都会在加载的第一时间就保存下这些 JavaScript 固有对象的方法引用,后续用户代码、第三方库对于这些内置对象的修改就不会再影响到他们。
而这也要求了固定的启动顺序,也就是说这些需要保存 JavaScript 固有方法的代码都需要第一时间加载,然后才能加载用户代码。比如社区包 get-intrinsic 提供了保存这些固有方法的能力,但是必须作为第一个包被引入。值得注意的是,如 get-intrinsic 等实现是通过遍历所有已知的 JavaScript 固有方法来实现的,也意味着他们的代码包大小是不可忽视的一个问题,如 get-intrinsic 包有9.7KB。
目前 JavaScript 有什么固有对象与方法?ECMA262: 已知的固有对象(https://tc39.es/ecma262/#sec-well-known-intrinsic-objects)
另外,有些 JavaScript 的固有方法是必须通过特定语法来获取的,比如 GeneratorFunction 与 AsyncGeneratorFunction:
const GeneratorFunction = (function* (){}).constructor;const AsyncGeneratorFunction = (asyncfunction* (){}).constructor;
get-intrinsic 如果需要能在低版本的 JavaScript 环境中也能正常运行,就必须通过 eval 等方式来间接执行 JavaScript 代码而避免整个文件因为不支持的语法而无法执行。而 eval 的问题也是显而易见的,它可能会与 CSP 冲突而无法使用。
所以提案期望引入一个内置的 JavaScript 函数来直接获取 JavaScript 的固有方法、对象,这样就不需要依赖于特定的语法就可以访问这些固有方法,给 Node.js 等运行时、各种 Polyfill 库都提供了便利性。
const GeneratorFunction = getIntrisic("%GeneratorFunction%");const AsyncGeneratorFunction = getIntrinsic("%AsyncGeneratorFunction%");

Fixed layout objects

提案链接:https://github.com/syg/proposal-structs/
近期,关于 WebAssembly GC 提案的落地非常惹人注目。作为与 WebAssembly 交互的第一梯队语言,JavaScript 自然也需要一个良好的能力来与 Wasm GC 来交互。这里说的“交互”,目前提案将这个能力限制在了 JavaScript 能够与 Wasm 互相访问,并没有以两者底层实现共享为目标。
WasmGC 中的对象在 JavaScript 暴露的结构就是提案所描述的固定布局对象(或者说 struct)。不过在 Wasm 中这个对象的字段是有额外的类型检查的,而 JavaScript 中这个固定布局对象并没有类型检查、限制一说。JavaScript Struct 后续当然也可以反哺到 Wasm 中作为 WasmGC 的对象,不过这部分工作提案期望留给未来后续来设计,因为这可能需要引入 Struct 的字段类型检查与限制。
对象传递方向
Wasm -> JS
JS -> Wasm
结构体实例
这个提案的目标
未来提案的目标
类型与反射
不是 TC39 的目标
不是 TC39 的目标
另外,大型应用如 GSuite(Google 套件)、Microsoft Office、TypeScript Compiler 都或多或少地碰到了性能问题,他们希望能在 JavaScript 中有更方便的内存共享、多线程工作负载的方式来提升应用表现。在此之前,在 JavaScript 中我们已经可以使用 SharedArrayBuffer 来共享内存。不过,使用 SharedArrayBuffer 操作复杂对象数据时,通常我们还需要通过一层包装来序列化与反序列化,有额外的成本。如果我们能直接在不同的 JavaScript 执行上下文里直接共享一系列的对象、结构体的话,就能以更具有表达能力的方式来操作共享内存。
目前,提案所提出的语法与能力都还是处于草稿阶段,提案提出了两种类型的 struct,第一种既是普通、非共享内存型。与 class 实例、普通 JavaScript 对象的区别是他们默认就是已经 seal 的对象 Object.seal(obj)  (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/seal),不能动态添加新的字段:
struct classBox{ x; getX() { returnthis.x; }}let box = new Box();// x 已经声明过box.x = 42;// 返回 42box.getX();// Structs 相当于 Object.seal 过的对象,不能添加新字段assert.throws(() => { box.y = 8.8;});
另外一种,就是可以用于 Web API 中的 MessagePort 等 API 传递到其他 JavaScript 执行环境中的共享内存型 struct。相比于普通的 struct 来说,共享的 struct 不能有 constructor 以外的方法,不过可以有静态方法:
shared struct classSharedBox{constructor(x) { this.x = x; } x;}let sharedBox = new SharedBox();// x 声明过sharedBox.x = 42;assertThrows(() => {// y 没有声明过 sharedBox.y = 84;});// 将共享 struct 传递给 workerlet worker = new Worker('worker.js');worker.postMessage({ sharedBox });// 字段类型可以随意变更,就如同普通 JavaScript 对象一样sharedBox.x = "main";
结语
由贺师俊牵头,阿里巴巴前端标准化小组等多方参与组建的 JavaScript 中文兴趣小组(JSCIG,JavaScript Chinese Interest Group)在 GitHub 上开放讨论各种 ECMAScript 的问题,非常欢迎有兴趣的同学参与讨论:esdiscuss

关注「Alibaba F2E」微信公众号把握阿里巴巴前端新动向
继续阅读
阅读原文