大家好,我是 ssh
。
近日,ECMA 国际技术委员会 39(TC39)在东京召开了第 104 次大会,讨论了多项
ECMAScript
(JavaScript)提案的进展情况,批准了其中多项提案进入下一个阶段。
「Stage 4」迭代器助手(Iterator Helpers)
「Stage 4」导入属性与 JSON 模块(Import Attributes & JSON Modules)
「Stage 4」正则表达式修饰符(Regular Expression Modifiers)
「Stage 3」精确求和(Math.sumPrecise)
「Stage 2.7」迭代器序列化(Iterator Sequencing)
「Stage 2」结构体与共享结构体(Structs & Shared Structs)
「讨论中」不可变的 ArrayBuffer(Immutable ArrayBuffers)
Stage 2.7
在介绍这些提案前,我们先聊聊 ECMAScript 的提案流程引入了一个新阶段 — 2.7 阶段。
每个新特性在被正式纳入 JavaScript 规范之前,需要通过一个提案流程。这一流程从 0 阶段(初步想法)一直到 4 阶段(准备发布)。所有提案流程都是以零为编号开始的,通常包含 0(草案提案)、1(提案通过)、2(特性定义)、3(推荐实施)、和 4(完成并发布)。
2 阶段
:达成共识并定义了可能的解决方案(改进和优化)。
2.7 阶段的关键在于,它相当于过去的 3 阶段,但更强调测试的编写和验证。提案进入 2.7 阶段时,设计已经完成,规范也已完整,此时需要编写实际代码(包括测试和非
polyfill
实现)来获取反馈,以便进一步推进。
在 2023 年底,TC39 正式引入了 2.7 阶段。这个阶段源于对提案流程的优化,希望能在提案进入 3 阶段前,确保所有的测试都已经编写并通过了验证。之前,3 阶段并不包含测试的内容,这可能导致当测试实现时发现新的问题,从而出现从 3 阶段退回到 2 阶段的情况。
为什么不直接增加一个新的阶段编号,而选择使用 2.7 呢?主要是为了避免大规模的文档更新和链接破损。如果将现有的阶段重新编号,比如将 3 阶段改为 4 阶段,可能会导致大量的文档和链接失效,维护成本会非常高。
下面是一些关键提案的详细介绍及其进展:
1. 「Stage 4」迭代器助手(Iterator Helpers)
迭代器在表示大型或无限可枚举数据集时非常有用。然而,迭代器缺乏与数组或其他有限数据结构同样易用的辅助方法,导致一些问题不得不通过数组或外部库来解决。许多库和编程语言已经提供了类似的接口。
该提案引入了一系列新的迭代器原型方法,允许开发者更方便地使用和消费迭代器。
应用映射函数,返回处理后的值的迭代器。
iter.map(value => value * value);
根据过滤函数筛选元素,返回通过条件的值的迭代器。
iter.filter(value => value % 2 == 0 );
获取有限数量的元素,返回新的迭代器。
iter.take(3 );
跳过指定数量的元素,返回剩余元素的新迭代器。
iter.drop(3 );
将映射函数作用于元素,并展平结果,返回扁平化后的新迭代器。
iter.flatMap(value => value.split(" " ));
reduce(reducer, initialValue)
通过 reducer 函数累计处理元素,返回汇总结果。
iter.reduce((sum, value ) => sum + value, 0 );
将迭代器转换为数组。
iter.toArray();
对每个元素执行副作用操作,不返回值。
iter.forEach(value => console .log(value));
检查是否有任意一个元素满足条件,返回布尔值。
iter.some(value => value > 1 );
检查是否所有元素都满足条件,返回布尔值。
iter.every(value => value >= 0 );
找到第一个满足条件的元素,返回该元素,没有找到返回
undefined
。
iter.find(value => value > 1 );
将“类似迭代器”的对象转换为迭代器。
Iterator.from(arrayLike);
GitHub 链接:Iterator Helpers Proposal
https://github.com/tc39/proposal-iterator-helpers
2. 「Stage 4」导入属性与 JSON 模块(Import Attributes & JSON Modules)
导入属性与 JSON 模块提案已进入 Stage 4,此提案增加了在导入文件时附带额外信息的能力。初始应用包括支持 JSON 模块,使开发者能够在导入 JSON 文件时明确指定其类型为
json
,增强代码的可读性和安全性。
标准化 JSON ES 模块的提案使得 JavaScript 模块可以轻松导入 JSON 数据文件,类似于许多非标准 JavaScript 模块系统中的支持。此提案不仅获得了 Web 开发者和浏览器的广泛支持,还被合并到了 HTML 标准中,由微软为 V8/Chromium 实现。然而,为了增强安全性,提出需要在导入 JSON 模块时使用语法标记,以防服务器意外返回不同 MIME 类型,导致意外代码执行。
为支持不同模块类型,标准化了以下语法:
// 静态导入 JSON 模块 import json from "./foo.json" with { type : "json" };// 动态导入 JSON 模块 import
("foo.json" , { with : { type : "json" } });
使用
with
语法可以在不同上下文中设置各种属性:
import json from "./foo.json" with { type : "json" };
export { val } from './foo.js' with { type : "javascript" };
import ("foo.json" , { with : { type : "json" } });
下面是一些使用场景示例
new Worker("foo.wasm" , { type : "module" , with : { type : "webassembly" } });
<script src ="foo.wasm" type ="module" withtype ="webassembly" > script >
import json from "./data.json" with { type : "json" };console .log(json); // JSON 数据
import ("./data.json" , { with : { type : "json" } }) .then(json => { console .log(json); // JSON 数据 });
new Worker("module.wasm" , { type : "module" , with : { type : "webassembly" } });
GitHub 链接:Import Attributes Proposal
https://github.com/tc39/proposal-import-attributes
3. 「Stage 4」正则表达式修饰符(Regular Expression Modifiers)
正则表达式修饰符提案已进入 Stage 4,该提案允许在子表达式内更改正则表达式的标志,从而使正则表达式变得更加灵活。
正则表达式标志是许多正则表达式引擎中常见的功能,用于解析器、语法高亮等工具。然而,在当前 JavaScript 中,这些标志要么全局启用,要么全局禁用,缺乏细粒度的控制能力。这个提案提出了让这些标志可以在子表达式范围内生效的机制。
该提案引入了在正则表达式中动态设置或取消各种标志的语法:
(?imsx-imsx:子表达式)
设置或取消从当前位置直到下一个关闭括号或表达式结尾的标志
(注意:这部分提案已不再被考虑)
(?imsx-imsx)
支持的标志包括:
s
- 单行模式(也称 "dot all" 模式)
示例:
const re1 = /^[a-z](?-i:[a-z])$/i ; re1.test("ab" ); // true re1.test("Ab" ); // true re1.test("aB" ); // false
const re2 = /^(?i:[a-z])[a-z]$/ ; re2.test("ab" ); // true re2.test("Ab" ); // true re2.test("aB" ); // false
https://github.com/tc39/proposal-regexp-modifiers
4. 「Stage 4」Promise.try
Promise.try
提案已进入 Stage 4,这个提案用于简化同步和异步函数的统一处理。它将任意函数包装在一个
Promise
中,确保函数在当前调用栈中执行,并返回一个
Promise
,处理可能的返回值或异常。
动机
现有问题
:使用
Promise.resolve().then(f)
会导致函数
f
异步调用,而
new Promise(resolve => resolve(f()))
使用不便。
解决方案
:
Promise.try(f)
提供了简洁的 API,同步执行函数,并处理生成的
Promise
。
主要功能
包装返回值或异常为
Promise
,支持链式操作。
同步函数:
function syncFunction ( ) { return 42 ; }Promise .try(syncFunction) .then(console .log) // 输出:42 .catch(console .error);
异步函数:
async function asyncFunction ( ) { return 42 ; }Promise .try(asyncFunction) .then(console .log) // 输出:42 .catch(console .error);
处理异常:
function riskyFunction ( ) { throw new Error ('Error!' ); }Promise .try(riskyFunction) .then(console .log) .catch(console .error); // 输出:Error: Error!
GitHub 链接:Promise.try Proposal
https://github.com/tc39/proposal-promise-try
5.「Stage 3」精确求和(Math.sumPrecise)
精确求和提案已进入 Stage 3,该提案建议在 JavaScript 数学库中增加一个新的静态方法
Math.sumPrecise
,用于精确计算多个浮点数的和,避免传统加法中的浮点数精度问题。
动机
常见操作
:对列表求和是非常常见的操作,目前很多情况依赖
Array.prototype.reduce
。
精度问题
:简单的
.reduce((a, b) => a + b, 0)
在处理浮点数时可能会有精度问题,通过更聪明的算法可以提高精度。
因此提议添加一个
Math.sumPrecise
相对于传统求和方法在精度上的改进:
let values = [1e20 , 0.1 , -1e20 ]; values.reduce((a, b ) => a + b, 0 ); // 0 Math .sumPrecise(values); // 0.1
GitHub 链接:Math.sumPrecise Proposal
https://github.com/tc39/proposal-math-sum-precise
6.「Stage 3」Atomics.pause
Atomics.pause(N)
提案已进入 Stage 3,该提案建议增加
Atomics.pause
方法,用于多线程编程中优化 CPU 资源利用。
Atomics.pause
可以在指定的纳秒时间内暂停当前线程,从而提高 CPU 使用效率。
在多线程编程中,锁的高效实现非常关键。当前的锁获取算法通常如下:
let spins = 0 ;do { if (TryLock()) { // 锁定成功 return ; } SpinForALittleBit(); spins++; } while (spins // 慢速路径 PutThreadToSleepUntilLockReleased();
对于这种情况,通过短暂的空转(spinning)可以提高性能,因为避免了线程进入内核。相反,在竞争激烈时,将线程置于休眠状态可以提高效率。然而,在 JavaScript 中编写优化的
SpinForALittleBit
方法非常困难。
“空转” 是计算机科学中的一个术语,英文通常叫做 “spinning” 或者 “busy waiting”。指的是一个线程或进程在等待某个条件满足期间,仍然保持在执行状态,而不进入阻塞状态(即不让出 CPU)。它会不断地检查这个条件,比如一个锁是否已经被释放。
提案中引入的新方法是
Atomics.pause(N)
,该方法执行一段非常短的有限等待时间,运行时可以通过适当的 CPU 提示来实现。它不具有阻塞性,因此可以在主线程和工作线程中调用。
以下是如何使用
Atomics.pause
进行空转的代码示例:
// 使用 Atomics.pause 进行空转 let spins = 0 ;do { if (TryLock()) { // 锁定成功 return ; } Atomics.pause(spins); spins++; } while (spins
CPU 提示
:不同架构可能有不同实现。以 x86 为例,Intel 推荐的
pause
指令和指数退避(exponential backoff)结合使用,可以实现有效的 CPU 提示。
控制参数 N
:非负整数参数
N
控制暂停时间,值越大,暂停时间越长。它可以用于在循环中实现退避算法。
GitHub 链接:Atomics.pause Proposal
https://github.com/tc39/proposal-atomics-microwait
7. 「Stage 2.7」Error.isError
Error.isError
提案已进入 Stage 2.7。
proposal-is-error
提案旨在为 JavaScript 引入一种新的方法
Error.isError
,用于可靠地判断一个值是否为原生
Error
对象。这将解决
instanceof Error
在跨上下文(如 iframe 或 Node.js 的
vm
模块)使用时可能导致的误判问题。
当前,判断一个对象是否为
Error
实例主要依赖于
instanceof Error
,但这种方法在跨越不同环境时并不可靠。不同 JavaScript 运行环境之间创建的错误对象实例无法通过
instanceof
进行可靠的验证。此外,
Symbol.toStringTag
也影响了通过
Object.prototype.toString
进行检验的可靠性。
调试
:在调试过程中,能确定一个值是否为原生错误,这对错误报告库非常有益。
序列化
:平台如 RunKit 需要安全地序列化值并在用户浏览器中重建或描述它们,品牌检查对此至关重要。
结构化克隆
:HTML 的
structuredClone
方法以及 Node.js 中的克隆方法对原生错误对象有特殊处理。JavaScript 程序需要一种方法来提前知道这种行为是否会被应用。
基本用法:
class CustomError extends Error {}const error = new Error ('This is an error' );const customError = new CustomError('This is a custom error' );const notAnError = {};console .log(Error .isError(error)); // 输出: true console .log(Error .isError(customError)); // 输出: true console .log(Error .isError(notAnError)); // 输出: false console .log(Error .isError(undefined )); // 输出: false
处理跨域实例:
// 假设我们有一个 iframe 引入的页面 const iframe = document .createElement('iframe' );document .body.appendChild(iframe);const iframeError = iframe.contentWindow.Error('This error comes from an iframe' );console .log(Error .isError(iframeError)); // 输出: true console .log(iframeError instanceof Error ); // 输出: false
GitHub 链接:Error.isError Proposal
https://github.com/tc39/proposal-error-iserror
8. 「Stage 2.7」迭代器序列化(Iterator Sequencing)
迭代器序列化提案已经进入 Stage 2.7。
在 JavaScript 编程过程中,我们经常会遇到需要依次消费多个迭代器中的值的情况,这就像它们是一个单独的迭代器一样。在其他语言以及一些迭代器库(例如标准库)中,通常有类似
concat
或
chain
的功能来实现这种需求。在当前的 JavaScript 中,可以通过生成器实现这一点,如下所示:
let lows = Iterator.from([0 , 1 , 2 , 3 ]);let highs = Iterator.from([6 , 7 , 8 , 9 ]);let lowsAndHighs = function * ( ) { yield * lows; yield * highs; }();console .log(Array .from(lowsAndHighs)); // [0, 1, 2, 3, 6, 7, 8, 9]
此外,我们还能通过生成器方法在迭代器之间插入即时值:
let digits = function * ( ) { yield * lows; yield 4 ; yield 5 ; yield * highs; }();console .log(Array .from(digits)); // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
为了使这种操作更方便和实用,TC39 提出了新的解决方案。
新的解决方案使用了
Iterator.concat
方法来连接多个迭代器:
let digits = Iterator.concat(lows, [4 , 5 ], highs);
对于一些特殊情况,例如无限多的迭代器,可以将
flatMap
与身份函数结合使用:
function * p ( ) { for (let n = 1 ;; ++n) { yield Array (n).fill(n); } }let repeatedNats = p().flatMap(x => x);
GitHub 链接:Iterator Sequencing Proposal
https://github.com/tc39/proposal-iterator-sequencing
9. 「Stage 2」结构体与共享结构体(Structs & Shared Structs)
结构体与共享结构体提案已进入 Stage 2。
该提案
proposal-structs
旨在为 JavaScript 引入固定布局的对象(结构体),以提高性能和并行处理能力。结构体的设计目标是为高性能应用提供更高的性能上限,并且使其容易进行静态分析:
结构体
:固定布局对象。类似于类实例,但具有更多限制,有助于优化和分析。
结构体实例在创建时采用封闭的完整性级别,即固定布局。无法添加新属性,也不能改变原型,所有声明的字段可写、可枚举和不可配置。
struct Box { constructor (x) { this .x = x; } x; }let box = new Box(0 ); box.x = 42 ; // x 是已声明的 // 下面的操作会抛出异常,因为结构体是封闭的 assertThrows(() => { box.y = 8.8 ; }); assertThrows(() => { box.__proto__ = {}; });
结构体只能继承其他结构体。
struct Point extends Box { constructor (x, y) { this .y = y; // this 值可以立即使用 super (x); // 调用父结构体构造函数 return {}; // 返回值被丢弃,无法覆盖返回 } distance(other) { return Math .sqrt((other.x - this .x) ** 2 + (other.y - this .y) ** 2 ); } y; }let p = new Point(1 , 2 );let fake = { x : 4 , y : 5 };// 方法是不可泛化的 assertThrows(() => Point.prototype.distance.call(fake, p)); p.distance(fake); // 允许,接收者是 Point