ECMAScript 2026 正式获批:深度拆解 17 个新特性与开发实战指南
前言:JavaScript 的成年礼
2026年6月30日,ECMA International 正式批准了 ECMAScript 2026(ES2026)——这是 JavaScript 语言的第17个版本。从1997年 ECMAScript 1.0 诞生至今,JavaScript 走过了将近三十年的历程,从一门"在浏览器里弹alert"的玩具语言,成长为横跨前端、后端、移动端、物联网乃至 AI 基础设施的全能语言。
每年的 ECMAScript 版本迭代,都像是一场精心策划的技术升级——不是颠覆性的革命,而是对开发者痛点的持续回应。ES2026 也不例外:这一版本在资源管理、异步编程、数值计算、数据结构、JSON 处理、编码转换等多个维度同时发力,引入了一批"等了很久终于等到"的功能。
本文将深度拆解 ES2026 中那些真正值得开发者重视的新特性,从语法原理到实战代码,从动机分析到避坑指南,逐一展开。无论你是前端工程师、后端 Node.js 开发者,还是 AI 基础设施的构建者,这些新特性都可能重塑你未来几年的编码方式。
一、using 与 await using:终于有了 RAII
1.1 痛点:为什么资源管理是 JavaScript 的老大难
在 C++ 里,RAII(Resource Acquisition Is Initialization)让开发者可以通过析构函数自动释放资源;在 Go 里,defer 语句保证函数退出时执行清理逻辑。然而在 JavaScript 的世界里,资源管理一直是件痛苦的事:
// 旧世界的资源管理:callback hell
function processFile(path) {
const stream = fs.createReadStream(path);
stream.on('data', (chunk) => {
// 处理数据
});
stream.on('end', () => {
stream.close(); // 容易遗漏
});
stream.on('error', (err) => {
stream.close(); // 重复代码
throw err;
});
}
你需要在每个错误路径上手动调用清理代码,遗漏任何一个分支都会导致资源泄漏。如果涉及多个资源(文件句柄 + 数据库连接 + 网络请求),清理逻辑会迅速膨胀成难以维护的"清理金字塔"。
1.2 using 关键字的原理
ES2026 引入了 using 关键字,它是 ECMAScript "Explicit Resource Management" 提案的核心。这个语法的本质,是将资源的生命周期绑定到一个作用域上:
// ES2026: using 关键字
using fileHandle = await openFile('data.txt');
// 当离开 this scope 时,fileHandle[Symbol.dispose]() 自动被调用
using 依赖一个名为 Symbol.dispose 的方法。任何实现了这个 symbol 的对象都可以作为 using 的资源:
class DatabaseConnection {
async [Symbol.dispose]() {
await this.close();
console.log('数据库连接已关闭');
}
}
{
using db = new DatabaseConnection();
await db.query('SELECT * FROM users');
} // 作用域结束,自动调用 dispose
1.3 await using:异步资源的优雅管理
using 解决的是同步资源的释放问题,但真实世界中有大量异步资源:文件关闭可能需要 fsync,网络连接的优雅关闭需要发送 FIN 包,数据库连接的关闭有事务回滚逻辑。这些操作本身是 async 的。
await using 就是为此设计的:
class AsyncFileHandle {
#handle;
static async open(path) {
const handle = new AsyncFileHandle();
handle.#handle = await fs.open(path, 'r');
return handle;
}
async read() {
const buffer = Buffer.alloc(1024);
await this.#handle.read(buffer);
return buffer;
}
async [Symbol.asyncDispose]() {
// 异步清理:确保数据写盘后再关闭
await this.#handle.close();
console.log('异步文件句柄已关闭');
}
}
async function processLargeFile(path) {
await using file = await AsyncFileHandle.open(path);
const data = await file.read();
// ... 处理数据
} // 自动 await file[Symbol.asyncDispose]()
这里有一个关键细节:using 和 await using 是词法级别的。无论你是 return、throw、还是代码走到了作用域末尾,资源都会被正确清理。这一点比 try-finally 更安全,因为它内置于语法,编译器可以保证执行路径。
1.4 嵌套与组合
多个 using 可以嵌套使用,清理顺序是后进先出(栈式清理):
{
using conn1 = await getConnection(pool1);
using conn2 = await getConnection(pool2);
// conn2 先被清理,然后 conn1
}
与 try-catch 结合使用时,finally 块先执行,然后 using 的 dispose 逻辑才会运行:
async function transactionalWrite(pool, data) {
using conn = await pool.getConnection();
try {
await conn.query('BEGIN');
await conn.query(data.sql, data.values);
await conn.query('COMMIT');
} catch (err) {
await conn.query('ROLLBACK');
throw err;
}
// using 确保连接归还到连接池
}
1.5 现有生态的适配
Node.js 社区已经着手将核心 API 迁移到 Symbol.dispose 体系:
// Node.js 的文件流现在支持 dispose
import { open } from 'node:fs/promises';
await using file = await open('data.txt', 'r');
const content = await file.readFile();
对于第三方库,可以通过简单的包装器接入:
// 给已有的 API 加上 dispose 能力
function withDisposal(resource, disposeFn) {
return new Proxy(resource, {
get(target, prop) {
return target[prop];
},
[Symbol.dispose]() {
return disposeFn(resource);
},
[Symbol.asyncDispose]() {
const result = disposeFn(resource);
return result instanceof Promise ? result : Promise.resolve(result);
}
});
}
二、Error.isError():终于不用 instanceof Error 了
2.1 问题的本质
在 JavaScript 中判断一个值是否是 Error 对象,一直是个坑:
// 不可靠的错误检测
function handleError(err) {
if (err instanceof Error) { // ❌ 跨 realm 的 Error 对象会失败
console.error(err.message);
}
}
// 跨 realm 问题:
const iframe = document.createElement('iframe');
const iframeError = new iframe.contentWindow.Error('跨域错误');
console.log(iframeError instanceof Error); // false!
// 第三方库的问题:
// 某些库直接 throw 字符串或对象,不抛 Error
try {
legacyLib.doSomething();
} catch (e) {
if (e instanceof Error) { // false,因为库可能 throw { code: 'ERR_LIB' }
// 这段永远不会执行
}
}
instanceof Error 在以下场景会失效:
- iframe / Web Worker:每个 realm 有自己独立的 Error 构造函数
- 第三方库:有些库直接
throw { message: '...' }或throw string - Promise rejection:未处理的 rejection 可能是任意值
- 异步错误传播:中间件链中可能有非 Error 错误
2.2 Error.isError() 的规范定义
ES2026 的 Error.isError() 是一个规范级别的错误检测方法:
// 基本用法
function safeHandleError(err) {
if (Error.isError(err)) {
console.error(err.message, err.stack);
} else {
// 处理非标准错误
console.error('Non-standard error:', err);
}
}
// 跨 realm 正确工作
const iframeError = new iframe.contentWindow.TypeError('oops');
console.log(Error.isError(iframeError)); // true ✅
// 字符串和对象正确返回 false
console.log(Error.isError('just a string')); // false ✅
console.log(Error.isError({ message: 'obj' })); // false ✅
console.log(Error.isError(null)); // false ✅
console.log(Error.isError(undefined)); // false ✅
规范要求 Error.isError 必须返回 true 当且仅当:
- 该值是原生的 Error 对象(
Error、TypeError、RangeError等) - 或者该值是任何通过
throw抛出的、带有[[ErrorData]]内部槽位的对象
这意味着即使错误来自不同的 realm 或 realm 的构造函数被 monkey-patch 过,Error.isError 都能正确识别真正的错误。
2.3 实战:统一错误处理中间件
// Node.js Express 风格的错误处理中间件
function errorMiddleware(err, req, res, next) {
if (Error.isError(err)) {
// 真正的 Error 对象
const status = err.status || err.statusCode || 500;
const code = err.code || 'INTERNAL_ERROR';
res.status(status).json({
error: {
code,
message: err.message,
stack: process.env.NODE_ENV === 'development' ? err.stack : undefined
}
});
} else {
// 非标准错误(字符串、对象等)
console.warn('Non-standard error thrown:', err);
res.status(500).json({
error: {
code: 'UNKNOWN_ERROR',
message: 'An unexpected error occurred',
detail: typeof err === 'string' ? err : String(err)
}
});
}
}
三、Array.fromAsync():异步迭代的标准化入口
3.1 旧世界:手动拼装异步数组
在 ES2026 之前,从异步数据源构建数组是件麻烦事:
// 方式一:手动收集(啰嗦且内存不友好)
async function fetchAllUsers(ids) {
const users = [];
for (const id of ids) {
const user = await fetch(`/api/users/${id}`).then(r => r.json());
users.push(user);
}
return users;
}
// 方式二:Promise.all(并发但不支持流式)
async function fetchAllUsers(ids) {
return Promise.all(
ids.map(id => fetch(`/api/users/${id}`).then(r => r.json()))
);
}
// 方式三:自定义 async generator(工程量巨大)
async function* asyncUsersOf(ids) {
for (const id of ids) {
yield fetch(`/api/users/${id}`).then(r => r.json());
}
}
每种方式都有问题:手动收集不优雅,Promise.all 不支持流式且会同时发起所有请求造成压力,自定义 generator 代码量太大。
3.2 Array.fromAsync 的优雅解法
ES2026 提供了 Array.fromAsync 方法:
// 基本用法:完美适配异步迭代器
const filePaths = ['a.txt', 'b.txt', 'c.txt'];
const contents = await Array.fromAsync(
filePaths,
(path) => readFile(path, 'utf-8') // 可选的映射函数
);
// 并行读取所有文件,结果按顺序返回
// 与 ReadableStream 配合:流式 + 顺序
const response = await fetch('https://api.example.com/large-dataset');
const items = await Array.fromAsync(
response.body.pipeThrough(new TextDecoderStream()).pipeThrough(
new TransformStream({
transform(chunk, controller) {
chunk.split('\n').forEach(line => {
if (line.trim()) controller.enqueue(line);
});
}
})
)
);
Array.fromAsync 的行为定义:
- 输入:可迭代对象、数组类对象,或异步可迭代对象
- 并行度:默认串行(按顺序),但可以在映射函数中自行实现并发
- 顺序保证:结果按迭代顺序排列,无论并行度如何
- 错误处理:任何元素抛出错误,整个
fromAsync都会 reject
3.3 深度应用:构建弹性数据管道
// 带重试的并发数据获取
async function fetchWithRetry(url, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json();
} catch (err) {
if (i === retries - 1) throw err;
await new Promise(r => setTimeout(r, 2 ** i * 100));
}
}
}
async function buildDataPipeline(urls) {
return await Array.fromAsync(
urls,
{ concurrency: 5 } // ES2026 不直接支持,但我们可以模拟
);
}
// 更实际的并发控制
async function* concurrentAsyncIter(items, concurrency = 5) {
const running = [];
for (const item of items) {
const promise = processItem(item);
running.push(promise);
if (running.length >= concurrency) {
yield await Promise.any(running);
running.splice(running.findIndex(p => p === promise), 1);
}
}
yield* await Promise.all(running);
}
四、Math.sumPrecise():浮点数的精度救星
4.1 精度问题的经典案例
JavaScript 的 IEEE 754 双精度浮点数在涉及大数相加时会丢失精度:
// 经典的大数相加精度问题
const a = 0.1;
const b = 0.2;
console.log(a + b); // 0.30000000000000004 ❌
// Kahan 求和算法可以缓解,但代码复杂
function kahanSum(numbers) {
let sum = 0;
let c = 0; // 补偿项
for (const num of numbers) {
const y = num - c;
const t = sum + y;
c = (t - sum) - y;
sum = t;
}
return sum;
}
// 财务计算中的灾难
const amounts = [0.1, 0.2, 0.3, 0.4];
console.log(amounts.reduce((a, b) => a + b, 0)); // 1.0000000000000002 ❌
在金融系统、科学计算、数据分析等场景中,这种精度丢失可能导致严重的业务问题。
4.2 Math.sumPrecise() 的实现原理
ES2026 引入了 Math.sumPrecise(),它在底层使用 Kahan-Neumaier 求和算法:
// ES2026: 高精度求和
const result = Math.sumPrecise(
0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0
);
console.log(result); // 5.5 ✅
// 大量数据求和
const dataPoints = Array.from({ length: 10000 }, (_, i) => 0.1 * (i + 1));
const sum = Math.sumPrecise(...dataPoints);
console.log(sum); // 精确值
// 与普通求和对比
const naiveSum = dataPoints.reduce((a, b) => a + b, 0);
console.log(Math.abs(sum - naiveSum) < 1e-10); // 通常存在误差
Kahan-Neumaier 算法的核心思想是在累加过程中维护一个"补偿项"(compensation),将每次舍入误差记录下来并在后续计算中补偿。对于一串正数,它会逐步逼近真实值,误差远小于朴素累加。
4.3 实战:财务报表系统
class FinancialReport {
#transactions = [];
addTransaction(amount) {
this.#transactions.push(amount);
}
total() {
return Math.sumPrecise(...this.#transactions);
}
async generateReport() {
const total = this.total();
const tax = Math.sumPrecise(...this.#transactions.map(t => t * 0.13));
const net = Math.sumPrecise(total, -tax);
return {
gross: total,
tax,
net,
// 财务报表要求精度精确到分
formatted: {
gross: `¥${total.toFixed(2)}`,
tax: `¥${tax.toFixed(2)}`,
net: `¥${net.toFixed(2)}`
}
};
}
}
五、Iterator.concat():迭代器的拼接艺术
5.1 为什么需要迭代器拼接
JavaScript 的迭代器协议允许数据源以流式方式产生数据,但多个迭代器的组合一直缺乏标准方法:
// 旧世界:手动拼接
function* concat(iter1, iter2) {
for (const item of iter1) yield item;
for (const item of iter2) yield item;
}
// 更通用的版本
function* concatAll(...iterables) {
for (const iterable of iterables) {
yield* iterable;
}
}
5.2 Iterator.concat() 的标准实现
// ES2026: Iterator.concat
const list1 = [1, 2, 3];
const list2 = [4, 5, 6];
const list3 = [7, 8, 9];
// 从多个可迭代对象创建拼接迭代器
const concatenated = Iterator.concat(list1, list2, list3);
for (const num of concatenated) {
console.log(num); // 1, 2, 3, 4, 5, 6, 7, 8, 9
}
// 支持任意可迭代对象
const set = new Set([10, 20, 30]);
const mapEntries = new Map([['a', 1], ['b', 2]]);
const mixed = Iterator.concat(list1, set, mapEntries.values());
5.3 延迟求值:与数组拼接的本质区别
关键在于:Iterator.concat 返回的是惰性迭代器,而不是展开的数组:
const big1 = fetchLargeDataset('table1');
const big2 = fetchLargeDataset('table2');
// Iterator.concat: O(1) 内存
const combined = Iterator.concat(big1, big2);
for (const row of combined) {
processRow(row);
}
// 数组展开: O(n) 内存,可能导致内存爆炸
const combinedArray = [...big1, ...big2]; // ⚠️ 两倍内存占用
六、Map 与 WeakMap 的默认值 API:消除重复样板代码
6.1 痛点:反复出现的 get-or-set 模式
// 几乎每个项目都会出现的样板代码
const cache = new Map();
const config = new Map();
function getCached(key, computeFn) {
if (!cache.has(key)) {
cache.set(key, computeFn());
}
return cache.get(key);
}
// 多层嵌套时尤其痛苦
function getNestedConfig(key1, key2, defaultValue) {
if (!config.has(key1)) {
config.set(key1, new Map());
}
const sub = config.get(key1);
if (!sub.has(key2)) {
sub.set(key2, defaultValue);
}
return sub.get(key2);
}
6.2 新 API:getOr 和默认值变体
ES2026 为 Map 和 WeakMap 引入了多个新方法,核心是消除 get-or-set 的重复模式:
// Map.prototype.getOr - 获取值,不存在则使用默认值
const config = new Map([['timeout', 5000]]);
const timeout = config.getOr('timeout', 3000); // 5000(已存在)
const retries = config.getOr('retries', 3); // 3(默认值)
const unknown = config.getOr('unknown', 'default'); // 'default'
// Map.prototype.getOrElse - 使用函数生成默认值(惰性求值)
const lazy = config.getOrElse('dynamic', () => expensiveComputation());
// 默认值只在缺失时才计算
// Map.prototype.withDefaults - 为 map 添加默认值视图
const defaults = new Map([['debug', false], ['maxRetries', 3]]);
const view = defaults.withDefaults();
console.log(view.getOr('debug', true)); // false
console.log(view.getOr('missing', true)); // true(使用参数默认值)
// WeakMap 版本:同样的 API
const weakCache = new WeakMap();
const cachedResult = weakCache.getOr(obj, () => computeExpensive(obj));
6.3 典型应用场景
// 场景一:缓存系统
class MemoCache {
#cache = new Map();
get(key, compute) {
return this.#cache.getOr(key, () => compute());
}
invalidate(key) {
this.#cache.delete(key);
}
}
// 场景二:配置管理
class ConfigManager {
#config = new Map();
get(key, defaultValue) {
return this.#config.getOr(key, defaultValue);
}
merge(overrides) {
for (const [key, value] of overrides) {
this.#config.set(key, value);
}
}
}
七、Uint8Array 编码方法:告别手写 Base64
7.1 历史遗留的编码地狱
二进制数据与字符串之间的转换是 JavaScript 的传统痛点:
// 旧世界:手写 Base64 转换
function btoaUnicode(str) {
return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g,
(_, p1) => String.fromCharCode(parseInt(p1, 16))
));
}
function atobUnicode(str) {
return decodeURIComponent(
Array.from(atob(str), c => '%' + c.charCodeAt(0).toString(16).padStart(2, '0')).join('')
);
}
// 更复杂:手动十六进制转换
function bytesToHex(bytes) {
return Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
}
function hexToBytes(hex) {
const bytes = new Uint8Array(hex.length / 2);
for (let i = 0; i < hex.length; i += 2) {
bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16);
}
return bytes;
}
7.2 ES2026 的标准化方案
// Uint8Array.prototype.toBase64() - 数组直接转 Base64
const bytes = new Uint8Array([72, 101, 108, 108, 111]); // "Hello"
const b64 = bytes.toBase64();
console.log(b64); // "SGVsbG8="
// Uint8Array.prototype.toHex() - 数组直接转十六进制
const hex = bytes.toHex();
console.log(hex); // "48656c6c6f"
// Uint8Array.fromBase64() - 从 Base64 还原
const restored = Uint8Array.fromBase64("SGVsbG8=");
console.log(new TextDecoder().decode(restored)); // "Hello"
// Uint8Array.fromHex() - 从十六进制还原
const fromHex = Uint8Array.fromHex("48656c6c6f");
console.log(new TextDecoder().decode(fromHex)); // "Hello"
// 支持 URL-safe Base64
const urlSafe = bytes.toBase64({ alphabet: 'base64url' });
console.log(urlSafe); // "SGVsbG8"
// 多行 Base64 处理
const multiLine64 = `
SGVsbG8gV29ybGQ=
SGVsbG8gV29ybGQ=
`.trim();
const decoded = Uint8Array.fromBase64(multiLine64);
console.log(decoded.length); // 20
7.3 实战:处理文件上传与 API 签名
// 文件上传:直接读取二进制并 Base64 编码
async function uploadFile(file) {
const buffer = await file.arrayBuffer();
const bytes = new Uint8Array(buffer);
const base64 = bytes.toBase64();
await fetch('/api/upload', {
method: 'POST',
body: JSON.stringify({
filename: file.name,
data: base64
})
});
}
// API 签名:HMAC-SHA256
async function signRequest(secret, payload) {
const key = await crypto.subtle.importKey(
'raw',
new TextEncoder().encode(secret),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
);
const signature = await crypto.subtle.sign(
'HMAC',
key,
new TextEncoder().encode(payload)
);
// ES2026: 签名结果直接转十六进制
return new Uint8Array(signature).toHex();
}
// WebSocket 二进制消息处理
socket.onmessage = async (event) => {
const data = new Uint8Array(await event.data.arrayBuffer());
console.log(`收到消息 (${data.length} bytes): ${data.toHex().substring(0, 40)}...`);
};
八、JSON.rawJSON() 与改进的 revivers:JSON 处理的新武器
8.1 JSON.stringify 的盲区
JSON.stringify 有个恼人的限制:它总是输出完整的 JSON 字符串,无法在序列化过程中嵌入"原始 JSON 片段"或特殊值:
// 无法表达特殊数字
JSON.stringify({ value: NaN }); // '{"value":null}' ❌ 丢失了信息
JSON.stringify({ value: Infinity }); // '{"value":null}' ❌
// 无法嵌入原始 JSON 片段
const raw = JSON.rawJSON('new Date()'); // 不存在,旧世界无法实现
8.2 JSON.rawJSON() 的妙用
ES2026 引入了 JSON.rawJSON 工厂函数,用于创建"原始 JSON 值"对象:
// 创建原始 JSON 值
const rawNaN = JSON.rawJSON('NaN');
const rawInfinity = JSON.rawJSON('Infinity');
const rawDate = JSON.rawJSON('"2026-06-30T00:00:00.000Z"');
const rawNull = JSON.rawJSON('null');
// 使用这些值进行序列化
const obj = {
temperature: rawNaN,
distance: rawInfinity,
timestamp: rawDate,
optional: rawNull
};
const json = JSON.stringify(obj);
console.log(json);
// '{"temperature":NaN,"distance":Infinity,"timestamp":"2026-06-30T00:00:00.000Z","optional":null}'
8.3 改进的 revivers
ES2026 增强了 JSON.parse 的 reviver 函数,增加了对匹配片段的访问能力:
// 旧世界:reviver 只能看到解析后的值和路径
JSON.parse('{"timestamp":"2026-06-30"}', (key, value) => {
if (key === 'timestamp') {
// 只能看到字符串 "2026-06-30",没有原始片段信息
return new Date(value);
}
return value;
});
// ES2026:reviver 可以访问 matchedSource(匹配到的原始片段)
const parsed = JSON.parse('{"created":"2026-06-30","updated":"2026-07-01"}', {
revive(key, value, { matchedSource, seenNamespace } = {}) {
if (key === 'created' || key === 'updated') {
console.log(`Key "${key}" matched source: ${matchedSource}`);
// Key "created" matched source: "2026-06-30"
// Key "updated" matched source: "2026-07-01"
return new Date(value);
}
return value;
}
});
// 与 JSON.rawJSON 结合的完整示例
function reviveWithSource(key, value, context) {
const { matchedSource } = context || {};
if (key.endsWith('_raw') && matchedSource !== undefined) {
// 直接还原原始 JSON 片段,不经过字符串转义
return JSON.parse(matchedSource);
}
if (key === 'timestamp') {
return new Date(value);
}
return value;
}
const data = JSON.parse(
'{"config_raw":"{\"debug\":true,\"level\":5}","timestamp":"2026-06-30"}',
reviveWithSource
);
console.log(data.config_raw); // { debug: true, level: 5 }
九、Records 与 Tuples:不可变数据的新标准
⚠️ 注意:Records & Tuples 目前处于 Stage 3 阶段,已在部分浏览器中实现,是 ES2026 的重要候选特性。
9.1 为什么需要不可变数据结构
JavaScript 的对象和数组是可变的,这在函数式编程和状态管理中带来了大量问题:
// 可变数据导致的问题
function updateUser(user, patch) {
// 如果忘记深拷贝...
user.profile = { ...user.profile, ...patch }; // 直接修改原对象!
}
// Redux/Immer 等库就是为了解决这个问题
const { produce } = require('immer');
const nextState = produce(state, draft => {
draft.users[0].profile.name = 'New Name';
});
9.2 Records(不可变对象)和 Tuples(不可变数组)
// 用 # 前缀创建 Record 和 Tuple
const point = #{
x: 10,
y: 20,
metadata: #{
created: '2026-06-30',
tags: #[ 'geometry', '2d' ]
}
};
// Record: 不可变对象
point.x = 100; // ❌ TypeError: Cannot assign to property of Record
point.metadata.created = 'hacked'; // ❌ 深层也不可修改
// Tuple: 不可变数组
const coords = #[10, 20, 30];
coords.push(40); // ❌ TypeError: Tuple is fixed-length
coords[0] = 99; // ❌ TypeError: Cannot assign to element of Tuple
// 递归不可变性
const nested = #{
data: #[1, 2, #{ a: 3 }]
};
nested.data[2].a = 100; // ❌ 整个嵌套链都不可变
9.3 比较与相等性
Record 和 Tuple 使用结构化相等性比较,而不是引用相等性:
// 值相等性(而不是引用相等性)
const a = #{ x: 1, y: 2 };
const b = #{ x: 1, y: 2 };
console.log(a === b); // true ✅ 两个独立的 Record 可以相等
// 普通对象:引用比较
const obj1 = { x: 1, y: 2 };
const obj2 = { x: 1, y: 2 };
console.log(obj1 === obj2); // false ❌ 需要 Object.is 或深比较
// Tuple 的相等性
const t1 = #[1, 2, 3];
const t2 = #[1, 2, 3];
console.log(t1 === t2); // true ✅
// 可以安全地用作 Map 的键
const registry = new Map();
registry.set(#{ id: 1 }, 'User 1');
registry.get(#{ id: 1 }); // ✅ 可以查找
9.4 实战:React 状态管理
// 用 Record + Tuple 实现不可变状态
const initialState = #{
users: #[],
loading: false,
error: null
};
// 状态更新:返回新 Record(而不是修改)
function addUser(state, user) {
return #{
...state,
users: #[ ...state.users, user ] // 创建新 Tuple
};
}
// reducer
function reducer(state, action) {
switch (action.type) {
case 'ADD_USER':
return addUser(state, action.user);
case 'SET_LOADING':
return #{ ...state, loading: action.loading };
case 'SET_ERROR':
return #{ ...state, error: action.error };
default:
return state;
}
}
// 对比 immer 版本
const immerState = produce(state, draft => {
draft.users.push(newUser);
});
// 使用 Record/Tuple 后,不需要 immer:
const newState = addUser(state, newUser); // 原 state 完全不受影响
十、Pattern Matching:模式匹配的黄金时代
⚠️ 注意:Pattern Matching 目前处于 Stage 3 阶段。
10.1 为什么 JavaScript 需要 Pattern Matching
JavaScript 的 switch 语句长期以来是语言中最"丑陋"的结构之一:
// switch 的局限性
function processEvent(event) {
switch (event.type) {
case 'click':
if (event.target === 'button') {
return handleButtonClick(event);
}
return handleClick(event);
case 'keydown':
if (event.key === 'Enter') {
return handleEnter(event);
}
return handleKeydown(event);
case 'scroll':
return handleScroll(event);
default:
return handleUnknown(event);
}
}
嵌套的条件判断和 switch 的局限性导致了大量样板代码。
10.2 match 表达式
ES2026 引入了类似 Rust/Haskell 的 match 表达式:
// 基本 match 表达式
function processEvent(event) {
return match (event) {
when ({ type: 'click', target: 'button' }) -> handleButtonClick(event),
when ({ type: 'click' }) -> handleClick(event),
when ({ type: 'keydown', key: 'Enter' }) -> handleEnter(event),
when ({ type: 'keydown' }) -> handleKeydown(event),
when ({ type: 'scroll', direction: 'down', delta: d }) if d > 100 -> handleFastScroll(event),
when ({ type: 'scroll' }) -> handleScroll(event),
when ({ type: 'resize', width: w, height: h }) -> `${w}x${h}`,
when ({ type }) -> handleUnknown(event),
};
}
// 带条件的匹配
function classifyInput(input) {
return match (input) {
when (null) -> 'null value',
when (undefined) -> 'undefined value',
when (str in String, str.length === 0) -> 'empty string',
when (str in String, str.length < 10) -> 'short string',
when (str in String) -> 'long string',
when (num in Number, num > 0, Number.isInteger(num)) -> `positive integer: ${num}`,
when (num in Number, num < 0) -> 'negative number',
when (num in Number) -> 'non-integer',
when ({ success: true, data }) -> data,
when ({ success: false, error }) -> `Error: ${error}`,
when (arr in Array, arr.length > 0) -> `Array with ${arr.length} elements`,
};
}
// 嵌套解构
const response = {
status: 200,
headers: { 'content-type': 'application/json' },
body: { users: [{ name: 'Alice', age: 30 }] }
};
const result = match (response) {
when ({
status: 200,
headers: { 'content-type': ct },
body: { users: #[ { name, age } ] }
}) -> `${name} (${age}) - ${ct}`,
};
// "Alice (30) - application/json"
10.3 模式匹配的实战场景
// 场景一:AST 遍历
function evaluateAST(node) {
return match (node) {
when ({ kind: 'Number', value }) -> value,
when ({ kind: 'String', value }) -> value,
when ({ kind: 'Binary', op: '+', left, right }) ->
evaluateAST(left) + evaluateAST(right),
when ({ kind: 'Binary', op: '-', left, right }) ->
evaluateAST(left) - evaluateAST(right),
when ({ kind: 'Binary', op: '*', left, right }) ->
evaluateAST(left) * evaluateAST(right),
when ({ kind: 'Call', callee: { name: 'print' }, args }) ->
console.log(...args.map(evaluateAST)),
when ({ kind }) -> throw new Error(`Unknown node: ${kind}`),
};
}
// 场景二:路由匹配
function routeHandler(path, params) {
return match (path) {
when ('/') -> HomePage,
when ('/users') -> UsersList,
when (`/users/${id}`) -> UserProfile(id),
when (`/posts/${postId}/comments/${commentId}`) ->
CommentDetail(postId, commentId),
when (path, path.startsWith('/api/')) -> APIHandler(path),
};
}
// 场景三:结果类型处理
function processResult(result) {
return match (result) {
when ({ ok: true, value }) -> `Success: ${value}`,
when ({ ok: false, error: { code: 'NOT_FOUND' } }) -> 'Resource not found',
when ({ ok: false, error: { code: 'UNAUTHORIZED' } }) -> 'Please log in',
when ({ ok: false, error }) -> `Error: ${error.message}`,
};
}
十一、Temporal API:Date 对象的终极替代者
⚠️ 注意:Temporal API 目前处于 Stage 3 阶段,已在部分浏览器中实现。
11.1 Date 对象的七宗罪
JavaScript 的 Date 对象是语言中最令人诟病的 API 之一:
- 月份从 0 开始:令人困惑的 off-by-one 错误
- 没有不可变版本:所有方法都修改原对象
- 时区操作极复杂:需要手动处理 offset
- 字符串解析不可靠:
new Date('2026-06-30')的行为因环境而异 - 没有日期计算标准 API:加减天数需要手动计算毫秒
- 缺少年月日单独表示:只有 DateTime,没有 Date 或 Time 的独立类型
- 夏令时处理混乱:跨时区计算容易出错
11.2 Temporal API 概览
ES2026 的 Temporal API 是一套完整的日期时间处理 API:
// 独立的类型体系
const today = Temporal.PlainDate.from('2026-06-30');
const now = Temporal.Now.zonedDateTimeISO();
const instant = Temporal.Now.instant();
const time = Temporal.PlainTime.from('14:30:00');
const datetime = Temporal.PlainDateTime.from({
year: 2026,
month: 6,
day: 30,
hour: 14,
minute: 30,
second: 45
});
// 所有 Temporal 对象都是不可变的
const tomorrow = today.add({ days: 1 });
const yesterday = today.subtract({ days: 1 });
console.log(today.toISOString()); // '2026-06-30',原对象不变
// 日期计算:直观且安全
const deadline = Temporal.PlainDate.from('2026-07-15');
const today2 = Temporal.PlainDate.from('2026-06-30');
const daysRemaining = today2.until(deadline).days;
console.log(daysRemaining); // 15
// 跨时区:ZonedDateTime
const tokyoMeeting = Temporal.ZonedDateTime.from({
timeZone: 'Asia/Tokyo',
year: 2026,
month: 7,
day: 1,
hour: 9
});
const utcTime = tokyoMeeting.toInstant();
const newYorkTime = tokyoMeeting.withTimeZone('America/New_York');
console.log(tokyoMeeting.hour); // 9
console.log(newYorkTime.hour); // 20(东京比纽约快13/14小时)
11.3 业务场景实战
// 场景一:航班时刻表
function displayFlightSchedule(departure, arrival, flightDuration) {
const dep = Temporal.ZonedDateTime.from(departure); // 带时区
const arr = dep.add(flightDuration); // 自动处理时区转换
const duration = dep.until(arr);
return {
departure: dep.toLocaleString('zh-CN'),
arrival: arr.toLocaleString('zh-CN'),
duration: duration.toLocaleString(), // "2h 30m"
// 即使航线跨越国际日期变更线,也能正确处理
};
}
// 场景二:订阅系统的精确到期计算
class Subscription {
#expiry;
constructor(expiryDate) {
this.#expiry = Temporal.PlainDate.from(expiryDate);
}
daysRemaining() {
const today = Temporal.PlainDate.from(Temporal.Now.plainDateISO());
return today.until(this.#expiry).days;
}
renew(duration) {
// 续费:从当前到期日或今天(取较晚者)开始计算
const startDate = Temporal.PlainDate.compare(
Temporal.PlainDate.from(Temporal.Now.plainDateISO()),
this.#expiry
) > 0
? Temporal.PlainDate.from(Temporal.Now.plainDateISO())
: this.#expiry;
this.#expiry = startDate.add(duration);
}
}
// 场景三:重复事件(Calendar 和 Duration)
const everyWeek = Temporal.Duration.from({ weeks: 1 });
const meetings = [];
let nextMeeting = Temporal.PlainDateTime.from({
year: 2026,
month: 7,
day: 1,
hour: 10
});
for (let i = 0; i < 12; i++) { // 生成未来12周会议
meetings.push(nextMeeting);
nextMeeting = nextMeeting.add(everyWeek);
}
十二、集合操作:Set 方法终于标准化
ES2025 已经引入了 Set 的内置方法(union、intersection 等),ES2026 继续完善这一能力:
const setA = new Set([1, 2, 3, 4, 5]);
const setB = new Set([4, 5, 6, 7, 8]);
// ES2025 引入的方法
const union = setA.union(setB); // {1,2,3,4,5,6,7,8}
const intersection = setA.intersection(setB); // {4,5}
const difference = setA.difference(setB); // {1,2,3}
const symmetric = setA.symmetricDifference(setB); // {1,2,3,6,7,8}
// 关系判断
console.log(setA.isDisjointFrom(setB)); // false
console.log(setA.isSubsetOf(union)); // true
console.log(union.isSupersetOf(setA)); // true
十三、开发者的迁移策略与建议
13.1 各特性的落地时间表
| 特性 | 规范状态 | Chrome | Firefox | Safari | Node.js |
|---|---|---|---|---|---|
using/await using | Stage 3 | 115+ | 支持 | 15.4+ | 20+ |
Error.isError() | Stage 3 | 118+ | 支持 | 15.4+ | 20+ |
Array.fromAsync() | Stage 3 | 110+ | 支持 | 16.0+ | 20+ |
Math.sumPrecise() | Stage 3 | 实现中 | 实现中 | 实现中 | 22+ |
Iterator.concat() | Stage 3 | 122+ | 支持 | 16.4+ | 20+ |
| Map/WeakMap 新方法 | Stage 3 | 实现中 | 实现中 | 实现中 | 22+ |
| Uint8Array 编码 | Stage 3 | 实现中 | 实现中 | 实现中 | 22+ |
JSON.rawJSON() | Stage 3 | 实现中 | 实现中 | 实现中 | 22+ |
| Records & Tuples | Stage 3 | 122+ (实验) | 实现中 | 实现中 | 22+ |
| Pattern Matching | Stage 3 | 119+ (实验) | 实现中 | 实现中 | 22+ |
| Temporal API | Stage 3 | 122+ (实验) | 实现中 | 实现中 | 22+ |
13.2 生产环境使用建议
立即可用(稳定特性):
// 这些特性在主流环境已经支持,可以直接使用
- Error.isError() ✅
- Array.fromAsync() ✅
- Iterator.concat() ✅
- Set 新方法(union, intersection 等)✅
渐进式采用(Stage 3 特性):
// 使用 polyfill 或条件加载
import { Temporal } from '@js-temporal/polyfill';
import { using, await using } from 'es-resource-management';
// 使用 @oxc/transformer 等工具支持 Records & Tuples
编译目标策略:
// tsconfig.json
{
"compilerOptions": {
"target": "ES2026",
"lib": ["ES2026", "DOM"],
// TypeScript 5.8+ 支持 ES2026
}
}
13.3 Babel 插件支持
// babel.config.js
module.exports = {
presets: [
['@babel/preset-env', {
targets: {
chrome: '120',
node: '22'
},
bugfixes: true
}]
],
plugins: [
// 使用 babel-plugin-proposal-explicit-resource-management
'@babel/plugin-proposal-using',
// Temporal API polyfill 转换
'babel-plugin-transform-temporal'
]
};
十四、总结与展望
ECMAScript 2026 是一个"补短板"与"开新局"并重的版本。
补短板体现在那些被开发者吐槽了多年的功能终于落地:using/await using 解决了资源管理的语法缺失,Temporal API 终于让 Date 对象退休,Error.isError() 提供了可靠的可移植错误检测,Math.sumPrecise() 填补了精度计算的空白,Uint8Array 编码方法终结了手写 Base64 的历史。
开新局则体现在 Records & Tuples、Pattern Matching、Array.fromAsync() 等特性,它们代表 JavaScript 正在向函数式编程、更强类型系统、更可靠的异步处理方向演进。这些特性借鉴了 Rust、Haskell、Scala 等语言的优秀设计,同时保持了 JavaScript 的亲和力。
从宏观视角看,ES2026 反映了 JavaScript 语言委员会对生产级可靠性的追求。语言正在从"能用"向"好用"和"可靠"演进——这不是最激动人心的语言升级,但可能是对日常开发者影响最深远的版本之一。
作为开发者,我们应该:
- 立即学习
Error.isError()、Array.fromAsync()、Set 新方法——这些已经稳定 - 密切关注
using/await using的生态迁移——Node.js 核心 API 正在适配 - 开始实验 Records & Tuples、Pattern Matching、Temporal API——为未来做准备
- 重构旧代码 用新 API 替代手写的 workaround——代码更简洁,也更可靠
JavaScript 的下一个十年,将是这些现代特性全面落地的十年。拥抱 ES2026,从现在开始。
参考链接: