作者 | 昭朗
阿里巴巴目前是 ECMA 的合作会员,阿里经济体前端委员会会有代表参加相关会议讨论,我们会持续为大家分享 TC39 核心会议上关于 ECMAScript 的最新进展,欢迎大家关注。
Stage 3 -> Stage 4
从 Stage 3 进入到 Stage 4 有以下几个门槛:
- 必须编写与所有提案内容对应的 tc39/test262 测试,用于给各大 JavaScript 引擎和 transpiler 等实现检查与标准的兼容程度,并且 test262 已经合入了提案所需要的测试用例;
- 至少要有两个实现能够兼容上述 Test 262 测试,并发布到正式版本中;
- 发起了将提案内容合入正式标准文本 tc39/ecma262 的 Pull Request,并被 ECMAScript 编辑签署同意意见。
Promise.any & AggregateError
提案链接
目前 Promise 主要的组合操作会有:
一个简单的 Promise.any 的使用例子:
Promise.any([
fetch('https://example.com/').then(() => 'home'),
fetch('https://example.com/blog').then(() => 'blog'),
fetch('https://example.com/docs').then(() => 'docs')
]).then((first) => {
// 任一 Promise 被 resolve.
console.log(first);
// → 'home'
}).catch((error) => {
// 当上述所有的 Promise 都被 reject.
console.log(error);
});
从例子中我们也可以看到,Promise.any 只有在所有的输入的 Promise 都被 reject 后才会被 reject,那么它的 catch 所获取到的参数会如何表示 reject Promise.any 的所有异常信息呢?
这即是这个提案另一个部分 AggregateError。AggregateError 由一系列 Error 组成,可以通过 AggregateError.errors 属性获取这些 Error 实例。
WeakRefs & FinalizationRegistry
提案链接
在 ES6 中引入的 WeakMap 和 WeakSet 也可以来实现的某种程度的弱引用,不过这个弱引用不是真的弱引用,本质上其实是 Ephemeron 表。在 Ephemeron 的实现中,key 是对象,只要 key 一直还在那么 value 就不会释放。而 WeakRef 则没有这个问题,通过 WeakRef 生成的引用,存储在普通的 Map 中时也可以正常释放。
在此之前,JavaScript 里引用释放没有回调触发。新的 WeakRef 提案里有类似析构函数的 FinalizationRegistry 支持。FinalizationRegistry 与析构机制的区别在于,析构函数能够拿到当前对象,而 FinalizationRegistry 中注册的回调并不能拿到被释放(或即将被释放)的对象,而是一个在往 FinalizationRegistry 中注册这个对象时,一同传入的 heldValue 参数。通过这个 heldValue 我们能够获知是哪一个对象被释放并触发了这一次的终结回调。
const cache = new Map();
const finalizationGroup = new FinalizationRegistry((name) => {
const ref = cache.get(name);
if (ref !== undefined && ref.deref() === undefined) {
cache.delete(name);
}
});
function getImageCached(name) {
const ref = cache.get(name); // 1
if (ref !== undefined) { // 2
const deref = ref.deref();
if (deref !== undefined) return deref;
}
const image = performExpensiveOperation(name); // 3
const wr = new WeakRef(image); // 4
cache.set(name, wr); // 5
finalizationGroup.register(image, name); // 6
return image; // 7
}
Logical Assignment
提案链接
这个提案定义了逻辑操作并赋值表达式,包含新增的三个逻辑操作并赋值操作符,逻辑或并赋值 ||,逻辑与并复制 &&,空值赋值 ??=:
// "Or Or Equals"
a ||= b;
a || (a = b);
// "And And Equals"
a &&= b;
a && (a = b);
// "QQ Equals"
a ??= b;
a ?? (a = b);
function example(opts) {
opts.foo = opts.foo ?? 'bar';
opts.baz ??= 'qux';
}
NumericLiteralSeparator
提案链接
非常长的数字字面量都非常难以阅读,特别是有非常多重复的数字,甚至需要借助外部工具来辅助阅读????。
1000000000 // 十万?百万?千万?
const FEE = 12300;
// 应该读成 12,300 吗? 或者因为它的单位是分(人民币),所以应该读成 123 元?
这个提案让我们可以使用 _ (U+005F) 作为长数字字面量的分隔符,让数字字面量更加易读。
10_0000_0000 // 所以这是十亿
let fee = 123_00; // ¥123元 (12300 分)
let amount = 123_4500; // 123万4500 (中文阅读习惯)
let amount = 1_234_500; // 1,234,500 (英文书写习惯)
// 同样可以用在小数部分与指数部分
0.000_001 // 百万分之一
1e10_000 // 10^(10 000)
Intl.ListFormat
提案链接
这是一个 ECMA 402 国际化提案。这个提案提供了基于语言的列表格式化。而列表格式化也是一个常见的本地化需求:
let lfmt = new Intl.ListFormat("zh", {
type: "conjunction", // "conjunction", "disjunction" or "unit"
style: "long", // "long", "short" or "narrow"
});
console.log(lfmt.format(["Anne", "John", "Mike"])); // "Anne、John和Mike"
Intl.DateTimeFormat dateStyle/timeStyle
提案链接
这个提案也是一个 ECMA 402 国际化提案。不同的语言、本地化偏好对于不同长度的日期时间格式化时,都有不同的偏好:
- en-US 格式短日期 7/27/20,相当于选项 year 为 2-digit,month 为 short,day 为 numeric;
- zh-CN 格式短日期 2020/7/27,相当于选项 year 为 numeric,month 为 numeric,day 为 numeric;
通过内置的标准化 dateStyle 和 timeStyle 选项,提供更加符合本地化配置的格式化,而不需要开发者自行根据本地化选项选择合适的格式化选项。
let dtf = new Intl.DateTimeFormat("zh", {
year: "numeric", // "numeric", "2-digit"
month: "numeric", // "short", "numeric", "2-digit"
day: "numeric", // "numeric", "2-digit"
});
console.log(dtf.format(new Date())); // "2020年7月27日"
// 通过新的 dateStyle/timeStyle 选项:
let dtf = new Intl.DateTimeFormat("zh", {
dateStyle: "short", // "full", "long", "medium" or "short"
});
console.log(dtf.format(new Date())); // "2020/7/27"
Stage 2 -> Stage 3
从 Stage 2 进入到 Stage 3 有以下几个门槛:1. 撰写了包含提案所有内容的标准文本,并有指定的 TC39 成员审阅并签署了同意意见;2. ECMAScript 编辑签署了同意意见。
iterator.items()
提案链接
很多时候,类似于 Python 中的数组负值索引可以非常实用。比如在 Python 中我们可以通过 arr[-1] 来访问数组中的最后一个元素,而不用通过目前 JavaScript 中的方式来访问 arr[arr.length-1]。这里的负数是作为从起始元素(即arr[0])开始的反向索引。
但是现在 JavaScript 中的问题是,[] 这个语法不仅仅只是在数组中使用(当然在 Python 中也不是),而在数组中也不仅仅只可以作为索引使用。像arr[1]一样通过索引引用一个值,事实上引用的是这个对象的 "1" 这个属性。所以 arr[-1] 已经完全可以在现在的 JavaScript 引擎中使用,只是它可能不是代表的我们想要表达的意思而已:它引用的是目标对象的 "-1"这个属性,而不是一个反向索引。
这个场景其实也不是第一次在TC39提出了,比如现在已经是 Stage 1 的 Array.prototype.lastItem 提案,而与 Array.prototype.lastItem 提案相比,这次这个提案提供了一个更加通用的方案,我们可以通过任意可索引的类型(Array,String,和 TypedArray)上的 .item 方法,来访问任意一个反向索引、或者是正向索引的元素。
Intl.Segmenter
提案链接
很多语言都有词分割与句分割。Unicode UAX 29 定义了文本元素的分割算法,可以在文本中找出不同文本元素的分界线(包括如中文,韩文,日文,泰文等基于词典分割的东亚语言)。这对于实现更加可靠的输入法、文本编辑器、文本处理都有非常大的帮助。
在 Unicode UAX 29 中定义的文本元素、词句分割算法的实现如果在浏览器、JavaScript 中原生实现的话,相比于开发者们引入自己的实现方案来说,可以节省非常多的带宽与内存。
let segmenter = new Intl.Segmenter("zh", {granularity: "word"});
// Use it to get an iterator for a string
let input = "我不是,我没有,你别瞎说。";
let segments = segmenter.segment(input);
// Use that for segmentation!
for (let {segment, index, isWordLike} of segments) {
console.log("segment at code units [%d, %d): «%s»%s",
index, index + segment.length,
segment,
isWordLike ? " (word-like)" : ""
);
}
// console.log output:
// segment at code units [0, 3): «我不是» (word-like)
// segment at code units [3, 4): «,»
// segment at code units [4, 5): «我» (word-like)
// segment at code units [5, 7): «没有» (word-like)
// segment at code units [7, 8): «,»
// segment at code units [8, 9): «你» (word-like)
// segment at code units [9, 10): «别» (word-like)
// segment at code units [10, 12): «瞎» (word-like)
// segment at code units [11, 12): «说» (word-like)
// segment at code units [12, 13): «。»
Stage 1 -> Stage 2
从 Stage 1 进入到 Stage 2 需要完成撰写包含提案所有内容的标准文本的初稿。
WeakRef CleanupSome
提案链接
WeakRef 与 FinalizationRegistry 允许 JavaScript 应用去观测垃圾回收的过程。这个观测性给实现带来了非常多潜在的问题,目前提案通过将可观测的时间点交由宿主环境如浏览器来决定,如什么时候 FinalizationRegistry 的回调会被调用等来规避这些问题。通常,宿主环境,如浏览器,都会通过 microtask queue 等基于循环队列的方式来实现这个方案:WeakRefs 从“可以观测到值”到无法观测到值“的变化会发生在每一个 microtask 检查点上(即所有的 Promise 任务执行完成);类似的,FinalizationRegistry 的 cleanup 回调也会在所有的 Promise 任务执行完成后被调用。所以,只有当我们把程序的执行权交还给事件队列后,WeakRef 与 FinalizationRegistry 才能按预期的行为工作。
而当场景结合上 Web Workers,SharedArrayBuffer,WebAssembly 等共享内存的场景,我们可能在一次执行中做非常多的计算工作、与其他执行环境通过共享内存与原子量交互的工作,这个过程中也没有将执行权交还给事件队列(或者很少将执行权交还给事件队列)。而 WebAssembly 的场景就非常匹配这个特殊情况。那么能不能有除了间断性地停止代码执行,交出执行权以外,其他的方法来让 WeakRef 与 FinalizationRegistry 工作呢?
这即是这个提案所期望解决的问题。FinalizationRegistry.prototype.cleanupSome接受一个函数作为参数(后续提案更新可能会直接使用 FinalizationRegistry 的 cleanup 回调),然后 JavaScript 实现可能会在后续调用这个回调,就如同 FinalizationRegistry 的 cleanup 回调一样,但是是以一个同步的方式完成。
值得注意的是,这个提案是从 WeakRefs 提案(Stage 3)中直接分离出来的提案,没有经过 Stage 1 的流程,而是直接作为 Stage 2 提案发起 Review。
Record and Tuple
提案链接
Record 与 Tuple 与通过 Object.free 冻结的对象与用户代码中的 class 对象的主要区别是 Record 与 Tuple 是原始类型。另外,Object 与 Array 的相等性取决于他们是否是同一个实例({} !== {}),而 Record 与 Tuple 是否相等取决于他们的值(#[0, 0] === #[0, 0])。除此之外,Record 与 Tuple 都可以使用与普通对象与数组一样的方式访问字段与元素,如((#[0, 0])[0] === 0)。
Record 与 Tuple 作为组合原始类型,同样可以作为原始类型在 JavaScript 中的各种场景使用:
const grid = new Map([
[#[0, 0], "player"],
[[0,0], "player"],
[#{x:3, y:5}, "enemy"],
[{x:3, y:5}, "enemy"],
]);
console.log(grid.get(#[0, 0])); // player
console.log(grid.get([0,0])); // undefined
console.log(grid.get(#{x:3, y:5})); // enemy
console.log(grid.get({x:3, y:5})); // undefined
JSON.parse source text access
JSON.parse is lossy, even with a reviver function. Replacer function output is subject to re-serialization
JSON.parse 无法对超出 IEEE 754 精度的数字精准解析—即使通过现在的 reviver 函数参数也不行。类似的,在 JSON.stringify 中,我们也无法通过 replacer 函数参数序列化 JSON 中不存在的类型。
// Numbers are subject to IEEE 754 precision limits.
JSON.parse(" 9999999999999999")
// → 10000000000000000
// …and reviver functions receive already-parsed values.
JSON.parse(" 9999999999999999", (key, val) => BigInt(val))
// → 10000000000000000n
// Strings get quoted
JSON.stringify(9999999999999999n, (key, val) => String(val))
// → "\"9999999999999999\""
// Unserializeable types get rejected
JSON.stringify(9999999999999999n, (key, val) => val)
// → TypeError
这个提案提出给 reviver 函数与 replacer 函数分别增加一个新的参数,原始 source 文本与一个 rawTag Symbol。其中,对于 replacer 函数的修改是这次会议新提出的内容,目前还没有非常详细的使用场景例子与 spec 文本,需要后续继续跟踪对于 replacer 的修改如何解决序列化的问题。
// Numbers are still subject to IEEE 754 precision limits.
JSON.parse(" 9999999999999999")
// → 10000000000000000
// …but reviver functions gain access to the raw source.
JSON.parse(" 9999999999999999", (key, val, {source}) => BigInt(source))
// → 9999999999999999n
// Emit a literal sequence of BigInt digits
JSON.stringify(9999999999999999n, (key, val, {rawTag}) =>
({[rawTag]: String(val)}) )
// → "9999999999999999"
Stage 0 -> Stage 1
从 Stage 0 进入到 Stage 1 有以下门槛:1. 找到一个 TC39 成员作为 champion 负责这个提案的演进;2. 明确提案需要解决的问题与需求和大致的解决方案;3. 例子;4. 对 API 形式、关键算法、语义、实现风险等有讨论、分析。
Stage 1 的提案会有可预见的比较大的改动,以下列出的例子并不代表提案最终会是例子中的语法、语义。
await operations
提案链接
目前,await 关键字对于一个全是 Promise 数组来说无法达成类似 co 的语义:
// co
const co = require('co');
co(function* () {
var result = yield Promise.all([ Promise.resolve(true) ]);
console.log(result) // => [ true ];
var result = yield [ Promise.resolve(true) ];
console.log(result) // => [ true ];
});
// async/await
(async function () {
var result = await Promise.all([ Promise.resolve(true) ]);
console.log(result) // => [ true ];
var result = await [ Promise.resolve(true) ];
console.log(result) // => [ Promise {true} ];
})();
暂且不论 co 激进的语义能否让开发者更好地理解、产生语义二义性。但是至少如果 await *如果同时具有 Promise.all 的语义,那我们就无法给 await 增加其他常见的 Promise 组合操作语义了,如 race,allSettled,any 等。
所以目前提案提出了如 await.all 等语法糖,让常见的 Promise 组合操作更加便于书写、便于阅读:
// before
await Promise.all(users.map(async x => fetchProfile(x.id)))
// after
await.all users.map(async x => fetchProfile(x.id))
Array.prototype.unique()
去重在数据处理中是一个非常非常常见的操作。我们可以通过 [...new Set(array)] 来比较方便地去重,但是这个方式只适用于原始量,如数字、字符串等,而无法对一系列复杂结构对象进行去重。
提案提出了一个可以接受一个函数作为获取去重标记的 Array.prototype.unique 方法,可以用于复杂结构对象的场景:
arr.unique()
// eq [...new Set(arr)] or …?
arr.unique(x => x.foo.bar.name)
// deduplicate by the provided function
ResizableArrayBuffer and GrowableSharedArrayBuffer
这个提案主要是期望给 WebAssembly 的场景提供更加方面的内存扩展方式。目前调整一个 ArrayBuffer 的大小需要复制内容,但是复制非常慢,而且可能导致内存空间碎片化。
提案提出了两种新的 ArrayBuffer 类型:ResizableArrayBuffer 和 GrowableSharedArrayBuffer。
ResizableArrayBuffer 是一个内部存储区域可以拆卸的 ArrayBuffer。设计上希望 ResizableArrayBuffer 可以原地调整大小,但是提案没有对调整大小是否能够观测做要求(改变地址等)。同样,提案目前也没有对调整大小的方案有做描述。
let rab = new ResizableArrayBuffer(1024, 1024 ** 2);
assert(rab.byteLength === 1024);
assert(rab.maximumByteLength === 1024 ** 2);
rab.resize(rab.byteLength * 2);
assert(rab.byteLength === 1024 * 2);
GrowableSharedArrayBuffer 是可以在多个执行环境*享的 ArrayBuffer,但是考虑到多个执行环境的同步,所以 GrowableSharedArrayBuffer 是一个只能增长而不能缩减大小的设计。
另外,提案目前对于可调整大小的 ResizableArrayBuffer 和 GrowableSharedArrayBuffer 的最大增长大小的边际条件既没有定义也没有最佳使用场景,会在接下来、和进入 Stage 2 后撰写 spec 文本的过程中,继续探索这些情况。
关注「Alibaba F2E」
把握阿里巴巴前端新动向