大家好,我是 ConardLi。
近日,ECMA 国际技术委员会 39(TC39)在东京召开了第 104 次大会,讨论了多项 ECMAScript
(JavaScript)提案的进展情况,批准了其中多项提案进入下一个阶段。
- 「Stage 4」迭代器助手(Iterator Helpers)
- 「Stage 4」导入属性与 JSON 模块(Import Attributes & JSON Modules)
- 「Stage 4」正则表达式修饰符(Regular Expression Modifiers)
- 「Stage 4」Promise.try
- 「Stage 3」精确求和(Math.sumPrecise)
- 「Stage 3」Atomics.pause
- 「Stage 2.7」Error.isError
- 「Stage 2.7」迭代器序列化(Iterator Sequencing)
- 「Stage 2」结构体与共享结构体(Structs & Shared Structs)
- 「Stage 2」Extractors
- 「讨论中」Array.zip
- 「讨论中」不可变的 ArrayBuffer(Immutable ArrayBuffers)
Stage 2.7
在介绍这些提案前,我们先聊聊 ECMAScript 的提案流程引入了一个新阶段 — 2.7 阶段。
每个新特性在被正式纳入 JavaScript 规范之前,需要通过一个提案流程。这一流程从 0 阶段(初步想法)一直到 4 阶段(准备发布)。所有提案流程都是以零为编号开始的,通常包含 0(草案提案)、1(提案通过)、2(特性定义)、3(推荐实施)、和 4(完成并发布)。
- 0 阶段:一个新的提案(构思和探索)。
- 1 阶段:提案进入考虑阶段(特性设计)。
- 2 阶段:达成共识并定义了可能的解决方案(改进和优化)。
- 2.7 阶段:编写测试(测试和验证)。
- 3 阶段:推荐实施提案(集成和兼容性探索)。
- 4 阶段:新特性准备纳入规范并发布!
2.7 阶段的关键在于,它相当于过去的 3 阶段,但更强调测试的编写和验证。提案进入 2.7 阶段时,设计已经完成,规范也已完整,此时需要编写实际代码(包括测试和非 polyfill
实现)来获取反馈,以便进一步推进。
在 2023 年底,TC39 正式引入了 2.7 阶段。这个阶段源于对提案流程的优化,希望能在提案进入 3 阶段前,确保所有的测试都已经编写并通过了验证。之前,3 阶段并不包含测试的内容,这可能导致当测试实现时发现新的问题,从而出现从 3 阶段退回到 2 阶段的情况。
为什么不直接增加一个新的阶段编号,而选择使用 2.7 呢?主要是为了避免大规模的文档更新和链接破损。如果将现有的阶段重新编号,比如将 3 阶段改为 4 阶段,可能会导致大量的文档和链接失效,维护成本会非常高。
下面是一些关键提案的详细介绍及其进展:
1. 「Stage 4」迭代器助手(Iterator Helpers)
迭代器在表示大型或无限可枚举数据集时非常有用。然而,迭代器缺乏与数组或其他有限数据结构同样易用的辅助方法,导致一些问题不得不通过数组或外部库来解决。许多库和编程语言已经提供了类似的接口。
该提案引入了一系列新的迭代器原型方法,允许开发者更方便地使用和消费迭代器。
map(mapperFn)
应用映射函数,返回处理后的值的迭代器。
iter.map(value => value * value);
filter(filtererFn)
根据过滤函数筛选元素,返回通过条件的值的迭代器。
iter.filter(value => value % 2 == 0);
take(limit)
获取有限数量的元素,返回新的迭代器。
iter.take(3);
drop(limit)
跳过指定数量的元素,返回剩余元素的新迭代器。
iter.drop(3);
flatMap(mapperFn)
将映射函数作用于元素,并展平结果,返回扁平化后的新迭代器。
iter.flatMap(value => value.split(" "));
reduce(reducer, initialValue)
通过 reducer 函数累计处理元素,返回汇总结果。
iter.reduce((sum, value) => sum + value, 0);
toArray()
将迭代器转换为数组。
iter.toArray();
forEach(fn)
对每个元素执行副作用操作,不返回值。
iter.forEach(value => console.log(value));
some(fn)
检查是否有任意一个元素满足条件,返回布尔值。
iter.some(value => value > 1);
every(fn)
检查是否所有元素都满足条件,返回布尔值。
iter.every(value => value >= 0);
find(fn)
找到第一个满足条件的元素,返回该元素,没有找到返回undefined
。
iter.find(value => value > 1);
Iterator.from(object)
将“类似迭代器”的对象转换为迭代器。
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" } });
下面是一些使用场景示例
- Worker实例化
new Worker("foo.wasm", { type: "module", with: { type: "webassembly" } });
- HTML 中的 script 标签
<script src="foo.wasm" type="module" withtype="webassembly"></script>
- 静态导入 JSON 模块
import json from "./data.json" with { type: "json" };
console.log(json); // JSON 数据
- 动态导入 JSON 模块
import("./data.json", { with: { type: "json" } })
.then(json => {
console.log(json); // JSON 数据
});
- 导入 WebAssembly 模块
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)
支持的标志包括:
i
- 忽略大小写m
- 多行模式s
- 单行模式(也称 “dot all” 模式)x
- 扩展模式
示例:
-
- 忽略大小写的局部子表达式:
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
。
主要功能
- 同步执行函数
f
。 - 包装返回值或异常为
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 < kSpinCount);
// 慢速路径
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 < kSpinCount);
- 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
- 共享结构体
共享结构体是可以在多个代理间共享并并行访问的结构体,除了遵循上述结构体属性外,还具有以下特性:
- 只能继承其他共享结构体。
- 具有
null
原型。 - 不能包含实例方法或实例私有名称。
- 实例可以在不复制的情况下与其他代理通信。
// main.js
shared struct SharedBox {
x;
}
let sharedBox = new SharedBox();
let sharedBox2 = new SharedBox();
unsafe {
sharedBox.x = 42; // x 是已声明的
sharedBox.x = sharedBox2; // x 是已声明的且 rhs 是共享的
assertThrows(() => { sharedBox.x = {}; }) // rhs 不是共享结构体
}
// 可编程测试值是否可以共享
assert(Reflect.canBeShared(sharedBox2));
assert(!Reflect.canBeShared({}));
let worker = new Worker('worker.js');
worker.postMessage({ sharedBox });
unsafe {
sharedBox.x = "main"; // x 是已声明的且 rhs 是原语
console.log(sharedBox.x);
}
// worker.js
onmessage = function(e) {
let sharedBox = e.data.sharedBox;
unsafe {
sharedBox.x = "worker"; // x 是已声明的且 rhs 是原语
console.log(sharedBox.x);
}
};
- 互斥锁和条件变量
用于同步访问共享内存的高级抽象:
互斥锁 (Mutex)
:非递归互斥锁,适用于同步对共享内存的访问。条件变量 (Condition)
:用于管理协作线程之间的等待和通知机制。
shared struct MicrosoftSharePoint {
x;
y;
mutex;
}
let point = new MicrosoftSharePoint();
point.mutex = new Atomics.Mutex();
let worker = new Worker('worker_mutex.js');
worker.postMessage({ point });
// 假设此代理可以阻塞
unsafe {
using lock = Atomics.Mutex.lock(point.mutex);
point.x = "main";
point.y = "main";
}
unsafe {
using lock = Atomics.Mutex.lock(point.mutex);
console.log(point.x, point.y);
}
// worker_mutex.js
onmessage = function(e) {
let point = e.data.point;
unsafe {
using lock = Atomics.Mutex.lock(point.mutex);
point.x = "worker";
point.y = "worker";
}
};
GitHub 链接:Structs & Shared Structs Proposal
https://github.com/tc39/proposal-structs
10. 「Stage 2」Extractors
Extractors 提案已进入 Stage 2。
ECMAScript 当前缺乏在解构时执行用户定义逻辑的机制,这使得数据验证和转换需要多条语句才能完成。通过引入 Extractors,可以将此类逻辑封装到一个解构模式中,从而简化代码。
Extractors 对象通过 Symbol.customMatcher
方法,允许自定义解构逻辑,并在解构时调用该方法。该方法返回一个可迭代对象,指示匹配成功以及要提取的元素。
以下是使用 Extractors 的一些关键代码示例:
- 基本解构示例
const Foo = {
[Symbol.customMatcher](value) {
return [value]; // 简单返回值包装成数组
}
};
const x = [1, 2, 3];
const Foo(y) = x; // 使用 Foo 提取器进行解构
console.log(y); // 输出:1
- 处理不同类型的数据
const DateExtractor = {
[Symbol.customMatcher](value) {
if (value instanceof Date) {
return [value];
} else if (typeof value === 'string') {
return [new Date(value)];
} else {
throw new TypeError('Invalid date');
}
}
};
const data = '2024-10-13T06:41:07Z';
const DateExtractor(date) = data; // 使用 DateExtractor 提取器进行解构
console.log(date); // 输出:Sun Oct 13 2024 06:41:07 GMT+0000 (UTC)
- 结合嵌套和模式匹配使用
const InstantExtractor = {
[Symbol.customMatcher](value) {
if (value instanceof Temporal.Instant) {
return [value];
} else if (value instanceof Date) {
return [Temporal.Instant.fromEpochMilliseconds(+value)];
} else if (typeof value === 'string') {
return [Temporal.Instant.from(value)];
} else {
throw new TypeError();
}
}
};
const obj = {
createdAt: '2024-10-13T06:41:07Z',
modifiedAt: new Date('2024-10-14T08:00:00Z')
};
const {
createdAt: InstantExtractor(createdAt),
modifiedAt: InstantExtractor(modifiedAt)
} = obj;
console.log(createdAt); // 输出:2024-10-13T06:41:07Z
console.log(modifiedAt); // 输出:Mon Oct 14 2024 08:00:00 GMT+0000 (UTC)
GitHub 链接:Extractors
https://github.com/tc39/proposal-extractors
11.「讨论中」Array.zip
Array.zip
提案目前在讨论阶段。该提案建议为 Array
构造函数添加 Array.zip
和 Array.zipKeyed
两个静态方法。使用这些方法,开发者可以将多个数组交叉合并为一个数组,从而实现更便捷的数据处理。
示例代码:
const list1 = [1, 2, 3];
const list2 = ['a', 'b', 'c'];
console.log(Array.zip(list1, list2)); // [[1, 'a'], [2, 'b'], [3, 'c']]
GitHub 链接:Array.zip Proposal
https://github.com/tc39/proposal-array-zip
12.「讨论中」不可变的 ArrayBuffer(Immutable ArrayBuffers)
不可变的 ArrayBuffer
提案目前在讨论阶段,该提案建议允许创建内容不可改变的缓冲区,以提高内存使用的安全性和稳定性。这种缓冲区一旦创建,其内容将无法修改,从而避免了数据的意外更改。
示例代码:
const buffer = new ArrayBuffer(10); // 可变
const immutableBuffer = ArrayBuffer.immutable(10); // 不可变
GitHub 链接:Immutable ArrayBuffers Proposal
https://github.com/tc39/proposal-immutable-arraybuffer
如果你想加入高质量前端交流群,或者你有任何其他事情想和我交流也可以添加我的个人微信 ConardLi 。