2023 年 7 月 11 日 - 7 月 13 日,第 97 次 TC39 会议在挪威举行,下面就来看看在这次会议中哪些 ECMAScript 提案取得了新进展吧!
TC39 是一个推动 JavaScript 发展的技术委员会,由各个主流浏览器厂商的代表构成,其主要工作就是制定 ECMAScript 标准。TC39 每两个月举行一次会议。对于新提案,从提出到最后被纳入 ECMAScript 新特性,TC39 的规范中分为五步:
- Stage 0(strawman),任何TC39的成员都可以提交。
- Stage 1(proposal),进入此阶段就意味着这一提案被认为是正式的了,需要对此提案的场景与API进行详尽的描述。
- Stage 2(draft),这一阶段的提案如果能最终进入到标准,那么在之后的阶段都不会有太大的变化,因为理论上只接受增量修改。
- Stage 3(candidate),这一阶段的提案只有在遇到了重大问题才会修改,规范文档需要被全面的完成。
- Stage 4(finished),这一阶段的提案将会被纳入到ES每年发布的规范之中。
附: ECMAScript 2023(ES14)已于 6 月 27 日正式发布,详见 >>> 《ECMAScript 2023 正式发布,有哪些新特性?》
Stage 3
数组分组
该 提案[1] 用于简化数组(和可迭代对象)中的分组操作。数组分组是一种非常常见的操作,其将相似的数据组合成组允许开发者计算更高阶的数据集。
const array = [1, 2, 3, 4, 5];
// Object.groupBy 根据任意键对元素进行分组,这里通过奇偶数对元素进行分组。
Object.groupBy(array, (num, index) => {
return num % 2 === 0 ? 'even': 'odd';
});
// => { odd: [1, 3, 5], even: [2, 4] }
// Map.groupBy 返回一个 Map 对象,方便使用对象键进行分组。
const odd = { odd: true };
const even = { even: true };
Map.groupBy(array, (num, index) => {
return num % 2 === 0 ? even: odd;
});
// => Map { {odd: true}: [1, 3, 5], {even: true}: [2, 4] }
该提案提供了两个方法:Object.groupBy 和 Map.groupBy。前者返回一个没有原型的对象,可以方便地进行解构操作,并且可以防止与全局 Object 属性发生意外冲突。后者返回一个普通的 Map 实例,可以对复杂键类型进行分组(比如复合键或元组)。
Promise.withResolvers
当手动创建一个 Promise 时,用户必须传递一个执行器回调函数,该函数接受两个参数:
- resolve 函数,用于触发 Promise 的解决。
- reject 函数,用于触发 Promise 的拒绝。
如果回调函数可以嵌入调用一个最终触发解决或拒绝的异步函数(例如注册事件监听器),则这种方式可以很好地工作。
const promise = new Promise((resolve, reject) => {
asyncRequest(config, response => {
const buffer = [];
response.on('data', data => buffer.push(data));
response.on('end', () => resolve(buffer));
response.on('error', reason => reject(reason));
});
});
然而,通常开发人员希望在实例化 Promise 后配置其解决和拒绝行为。目前,这需要一个繁琐的解决方法,从回调范围中提取 resolve 和 reject 函数:
let resolve, reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
asyncRequest(config, response => {
const buffer = [];
response.on('callback-request', id => {
promise.then(data => callback(id, data));
});
response.on('data', data => buffer.push(data));
response.on('end', () => resolve(buffer));
response.on('error', reason => reject(reason));
});
开发人员可能还有其他要求,需要将 resolve/reject 传递给多个调用方,因此必须以这种方式实现:
let resolve = () => { };
let reject = () => { };
function request(type, message) {
if (socket) {
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
socket.emit(type, message);
return promise;
}
return Promise.reject(new Error('Socket unavailable'));
}
socket.on('response', response => {
if (response.status === 200) {
resolve(response);
}
else {
reject(new Error(response));
}
});
socket.on('error', err => {
reject(err);
});
Promise.withResolvers[2] 提案简单地在 Promise 构造函数中添加了一个静态方法,暂时称为 withResolvers
,该方法返回一个 Promise,并方便地公开其解决和拒绝函数。
const { promise, resolve, reject } = Promise.withResolvers();
源阶段导入
无论是对于 JavaScript 还是 WebAssembly,都需要能够更紧密地定制模块的加载、链接和执行,超出了标准的宿主执行模型。
- 对于 JavaScript,创建自定义加载器需要一种模块源类型,以便共享宿主的解析、执行、安全性和缓存语义。
- 对于 WebAssembly,WebAssembly 模块的导入和导出通常需要进行自定义的检查和封装,以便正确设置,这通常需要手动的获取和实例化工作,在当前的宿主 ESM 集成提案中没有提供相应支持。
通过将语法模块源导入支持作为新的导入阶段,可以创建一个基础机制,将模块的静态、安全性和工具化优势从 ESM 集成扩展到这些动态实例化用例。
该提案[3]允许ES模块从主机提供的编译后的模块源的反映表达式进行导入:
import source x from "<specifier>";
仅支持上述形式的导入,不支持命名导出和未绑定声明。
动态形式使用 import.<phase>:
const x = await import.source("<specifier>");
通过将阶段作为显式语法的一部分,可以在静态上下文中静态区分全动态导入和仅用于源的导入(无需处理依赖项)。
处理时间区域规范化的变化
ECMAScript中的时间区域依赖于IANA时区数据库(TZDB)的标识符,如America/Los_Angeles或Asia/Tokyo。该提案旨在改善开发人员在TZDB中更改时间区域的规范标识符(例如从Europe/Kiev到Europe/Kyiv)时的开发体验。
减少实现之间以及实现与规范之间的差异
- 已完成 - 简化处理时区标识符的抽象操作。
- 已完成 - 澄清规范以防止更多的分歧。
- 在 Temporal 广泛采用之前,帮助V8和WebKit更新13个过时的规范标识(如Asia/Calcutta,Europe/Kiev和Asia/Saigon),以免出现问题。
- 制定规范文本以减少实现之间的分歧。这一步需要在实现者和TG2(ECMA-402团队)之间找到共同点,讨论规范化应该如何工作。
减少标准化变化的影响
- 避免对链接进行可观察的跟随。如果标准化变化不会影响现有代码,那么未来的标准化变化就不太可能破坏Web。由于标准化是实现定义的,这个变化(或许会、也许不会;需要进一步研究)在Temporal第4阶段之后发布可能是安全的,但最好不要等太久。
Temporal.TimeZone.from('Asia/Calcutta');
// => Asia/Kolkata(Firefox上当前的Temporal行为)
// => Asia/Calcutta(建议:在将标识符返回给调用方时,不要遵循链接)
- 添加Temporal.TimeZone.prototype.equals方法。由于(5)会在创建TimeZone对象时停止标准化标识符,因此有一个直观的方法来判断两个 TimeZone 对象是否表示相同的时区。
// 更人性化的标准化相等性测试
Temporal.TimeZone.from('Asia/Calcutta').equals('Asia/Kolkata');
// => true
Stage 2
Time Zone Canonicalization[4]
JavaScript应用程序可能会变得非常庞大,以至于即使加载它们的初始化脚本,执行起来也会产生显著的性能开销。通常,这种情况发生在应用程序的生命周期较晚的阶段,往往需要进行大规模的改动以提高性能。加载性能是一个重要的改进领域,涉及预加载技术以避免瀑布效应,并使用动态导入进行模块的惰性加载。
尽管使用了这些技术解决了加载性能问题,但代码本身的编写方式仍会导致执行性能开销和CPU瓶颈在初始化过程中出现。
该提案[5]是引入一种新的导入语法形式,它将始终返回一个命名空间对象。在使用时,模块及其依赖项不会被执行,但会完全加载到可以执行的状态,然后才会认为模块图已加载完成。只有当访问该模块的属性时,才会执行相应的操作。
该API将使用以下语法:
// 或使用自定义关键字:
import defer * as yNamespace from "y";
Stage 1
DataView get/set Uint8Clamped 方法
现在只有其中 10 个具有DataView的 get/set 方法。
该提案[6]旨在添加DataView.prototype.getUint8Clamped和DataView.prototype.setUint8Clamped方法。
- getUint8Clamped(offset: number): number:从指定的偏移量读取一个8位无符号整数(Uint8Clamped)值,并返回该值。
- setUint8Clamped(offset: number, value: number): void:将一个8位无符号整数(Uint8Clamped)值写入到指定的偏移量。
可选链赋值
该提案[7]建议在赋值运算符左侧添加对可选链的支持:a?.b = c。在实际开发中,经常需要对对象的属性进行赋值,但前提是该对象确实存在。
通常的做法是使用if语句来保护赋值操作:
if (obj) {
obj.prop = value;
}
新语法和现有语法对比如下:
相关链接
[1]提案: https://github.com/tc39/proposal-array-grouping。
[2]Promise.withResolvers: https://github.com/tc39/proposal-promise-with-resolvers。
[3]提案: https://github.com/tc39/proposal-source-phase-imports。
[4]Time Zone Canonicalization: https://github.com/tc39/proposal-canonical-tz。
[5]提案: https://github.com/tc39/proposal-defer-import-eval。
[6]提案: https://github.com/tc39/proposal-dataview-get-set-uint8clamped。
[7]提案: https://github.com/tc39/proposal-optional-chaining-assignment。