ECMAScript 2026 新特性一览
ES2026 为 JavaScript 增加七项新功能:Array.fromAsync、Math.sumPrecise、Uint8Array 的 base64 和 hex 方法、JSON.parse 源文本访问,以及 getOrInsert。
Ecma International 于 2026 年 6 月 30 日批准了 ECMAScript 2026 语言规范,其中恰好包含七项新特性:Array.fromAsync、Error.isError、Math.sumPrecise、Uint8Array 上的 base64 与 hex 方法、Iterator.concat、JSON.parse 源文本访问,以及 Map 和 WeakMap 上的 getOrInsert。
每年这些方法都是悄然落地的,而我们大多数人第一次接触它们,往往是在删掉一个用了好几年的自研工具函数的那一天。这一次,大概会是某个 base64 工具函数。
如果你最近搜索过 ES2026,很可能会发现各种互相矛盾的特性清单,其中好几份还把 Temporal 或 using 关键字算了进去。那些清单是错的,本文会解释原因,然后逐一介绍真正落地的这七项特性:它解决了什么问题、它替代了哪些代码,以及取而代之的那一行写法。
权威依据是 Ecma 的批准公告,它确认 ES2026 为 ECMA-262 的第 17 版;以及 TC39 的已完成提案表,其中恰好有七行的预期发布年份(Expected Publication Year)为 2026。
要点速览
- ES2026 于 2026 年 6 月 30 日获批,恰好新增七项特性;Temporal 与 Explicit Resource Management 并不在其中。
Math.sumPrecise([1e17, 1, -1e17])返回1,而用reduce对相同的值求和会返回0。Uint8Array内置了toBase64()、toHex()、fromBase64()和fromHex(),可以移除一个常见依赖。JSON.parse的 reviver 现在会收到第三个参数,暴露原始源文本;JSON.rawJSON()则修复了 stringify 一侧的问题,因此 BigInt 可以完整地往返于 JSON。Map.prototype.getOrInsert(key, value)在键已存在时返回已有值;只有键不存在时才会插入你提供的默认值。
哪些特性没能进入 ES2026?
Temporal 和 Explicit Resource Management(using / await using)不属于 ES2026。在 TC39 已完成提案列表中,两者的预期发布年份都是 2027,与 Atomics.pause 和 Joint Iteration 并列。这四项提案都是在 ES2026 的截止时间之后才进入 stage 4(TC39 流程的最后一步)的。2025 年底获批前的大量报道普遍预测它们会在 2026 年落地,这正是那些互相矛盾的特性清单的来源。任何人都可以做的核对方式是:筛选已完成提案表中的“预期发布年份”一列。有七项提案标注为 2026,其中没有一项是 Temporal。
Array.fromAsync
Array.fromAsync 接受一个异步迭代器,返回一个包裹在 promise 中的数组。它同样接受产出 promise 的同步迭代器、类数组对象,以及一个可选的映射函数,与 Array.from 的行为一致。在此之前,收集异步迭代器的内容意味着写一个 for await 循环,不断往可变数组里 push:
const arr = [];
for await (const n of asyncGen(1, 3)) {
arr.push(`page ${n}`);
}
现在同样的结果只需一个表达式:
const arr = await Array.fromAsync(asyncGen(1, 3), (n) => `page ${n}`);
// [ 'page 1', 'page 2', 'page 3' ]
Error.isError
只有当捕获到的值是真正的 Error 对象时,Error.isError(value) 才返回 true。JavaScript 允许你 throw 任何东西,而一旦值跨越 iframe 或 realm 边界,instanceof Error 就会产生误判,因为每个 realm 都有自己的 Error 构造函数。以 instanceof Error 作为判断条件的 catch 块,可能会静默跳过那个本应渲染有用信息的分支,而这类 bug 的会话回放呈现出一种熟悉的形态:用户卡在一个什么都不显示的界面上,而底层其实真的抛出了异常。Error.isError 弥补了这个缺口。
try {
throw new Error("session upload failed");
} catch (error) {
Error.isError(error); // true
}
try {
throw "session upload failed";
} catch (error) {
Error.isError(error); // false
}
Math.sumPrecise
Math.sumPrecise 对一个数字可迭代对象求和,且不会在中间结果中累积浮点误差。reduce 链式调用是两两相加,因此量级相差很大的数值会发生严重的相互抵消:
const values = [1e17, 1, -1e17];
values.reduce((a, b) => a + b, 0); // 0
Math.sumPrecise(values); // 1
reduce 版本把 1 完全丢掉了。除了精度问题,这也让求和 reducer 这类样板代码彻底退场。
Uint8Array 的 toBase64、toHex、fromBase64 与 fromHex
ES2026 为 Uint8Array 提供了内置的 toBase64() 和 toHex() 方法,以及用于解码的静态方法 Uint8Array.fromBase64() 和 Uint8Array.fromHex()。此前,二进制与文本之间的转换意味着引入一个库或手写编码逻辑;现在大多数代码库都可以删掉这个依赖了。
const value = new Uint8Array([79, 112, 101, 110, 82, 101, 112, 108, 97, 121]);
const valueBase64 = value.toBase64(); // 'T3BlblJlcGxheQ=='
const decoded = Uint8Array.fromBase64(valueBase64);
new TextDecoder().decode(decoded); // 'OpenReplay'
Iterator.concat
Iterator.concat() 将多个迭代器按顺序串联起来。在此之前,唯一的做法是手写一个生成器,遍历每个来源并用 yield* 委托:
function* combine(...iterators) {
for (const source of iterators) {
yield* source;
}
}
现在只需一次调用,而且普通的值数组可以夹在迭代器之间。下面中间的 [300] 就是一个普通数组,不需要用 Iterator.from 包装。
const iOne = Iterator.from([100, 200]);
const iTwo = Iterator.from([400, 500]);
const combined = Iterator.concat(iOne, [300], iTwo);
Array.from(combined); // [ 100, 200, 300, 400, 500 ]
JSON.parse 源文本访问
在 ES2026 之前,JavaScript 中 JSON 的往返转换在两个方向上都是有损的。解析一个大整数字面量会被静默取整,而 stringify 一个 BigInt 会抛出异常:
JSON.parse("9007199254740993"); // 9007199254740992
JSON.stringify(9007199254740993n); // TypeError: Do not know how to serialize a BigInt
JSON.parse 源文本访问提案修复了这两侧的问题。JSON.parse 现在会向 reviver 传入第三个参数,其中包含未经处理的 source 文本,你可以自行转换而不丢失数字位数。在 stringify 一侧,JSON.rawJSON() 让 replacer 能够原样输出一个值:
JSON.parse("9007199254740993", (key, value, { source }) => BigInt(source));
// 9007199254740993n
JSON.stringify(9007199254740993n, (key, value) => JSON.rawJSON(value));
// 9007199254740993
现在 BigInt 可以经历一次完整的 JSON 往返,既不丢位数也不抛异常。
Map.prototype.getOrInsert 与 WeakMap.prototype.getOrInsert
Upsert 提案为 Map 和 WeakMap 都带来了 getOrInsert(key, value),你不再需要先调用 has() 再调用 set()。这里的语义很关键:当键已存在时,getOrInsert 返回已有的值。只有当键不存在时,它才会插入并返回你提供的默认值。
const limits = new Map();
limits.set("uploads", 25);
limits.getOrInsert("exports", 10); // 10 (inserted)
limits.getOrInsert("uploads", 100); // 25 (existing value wins)
注意 100 被丢弃了,因为 "uploads" 已经被设置过。请把第二个参数当作默认值,而不是覆盖值。
现在你可以删掉什么?
ES2026 是一个“清理型”版本:这七项特性中的每一项都让你今天生产环境里很可能存在的某种写法退场。base64 工具依赖、求和 reducer、has() 然后 set() 的保护性代码块、用 yield* 拼接的生成器,以及 for await 收集循环,都有了直接的内置替代方案。各引擎的运行时支持情况不一,因此在移除 polyfill 之前,请查阅 MDN 的标准内置对象参考,了解每个方法当前的支持状况。然后从你代码库中最容易的那一项开始动手——对大多数团队来说,就是把 Map 的存在性检查换成 getOrInsert。
常见问题
Array.fromAsync 和 Promise.all 有什么区别?
Array.fromAsync 会按顺序逐个 await 值,并接受异步可迭代对象、产出 promise 的同步可迭代对象以及类数组对象。Promise.all 会并发执行一个 promise 可迭代对象,并在任意一个 promise 被拒绝时立即拒绝。需要并行执行时用 Promise.all;当顺序很重要或数据源是异步迭代器时用 Array.fromAsync。两者都返回一个解析为数组的 promise。
对 TypeError 和其他 Error 子类,Error.isError 会返回 true 吗?
会。Error.isError 对每一种真正的 Error 子类都返回 true,包括 TypeError、RangeError、SyntaxError 以及继承自 Error 的自定义类,因为引擎检查的是真实错误所携带的内部标记(brand),而不是原型链。在浏览器中,DOMException 也算作真实错误。对于只是模仿错误的对象,例如带有 message 属性的普通对象,或用 Object.create(Error.prototype) 创建的对象,它会返回 false。
Uint8Array.prototype.toBase64 能生成 URL 安全的 base64 吗?
可以。toBase64 接受一个选项对象,其中 alphabet 属性可设为 'base64'(默认值,使用 + 和 /)或 'base64url'(使用 - 和 _),另外还有一个 omitPadding 布尔值用于去掉尾部的 = 字符。Uint8Array.fromBase64 接受同样的 alphabet 选项,以及一个 lastChunkHandling 选项('loose'、'strict' 或 'stop-before-partial'),用于控制如何解码长度不足四个字符的最后一个分块。
如何避免在 getOrInsert 中计算开销较大的默认值?
使用 getOrInsertComputed(key, callback),同一个 Upsert 提案也为 Map 和 WeakMap 添加了该方法。它只在键不存在时调用回调来生成值,然后插入并返回结果;当键已存在时,回调根本不会执行,而是直接返回已有的值。而普通的 getOrInsert 总会在调用之前先对第二个参数求值,即便键已经存在。
Complete picture for complete understanding
Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.
Star on GitHub12k