ES2026 深度实战:Temporal 终结 Date 时代、using 资源管理与 Set 家族全面进化——JavaScript 十年来最大语法升级完全指南
引言:为什么 ES2026 值得每个前端开发者认真对待
如果你从 ES6(2015)开始写 JavaScript,你可能已经习惯了每年一次的 TC39 规范更新——大部分时候是小修小补,一个新方法、一个语法糖,翻翻 MDN 就完了。
但 ES2026 不一样。
这一次,TC39 一次性推进了至少五项重量级提案进入 Stage 4(正式标准):Temporal API 彻底替代已有 29 年历史的 Date 对象;using / await using 引入了 C# 风格的确定性资源管理;Set 方法家族 补齐了集合运算的百年欠账;Promise.try 让同步异常不再逃离异步边界;还有 Iterator Helpers 让迭代器链式操作成为一等公民。
这不是"今年多了几个 API"那种级别的更新——这是 JavaScript 语言设计哲学的一次范式跃迁。从"手动管理一切"到"语言帮你兜底",从"凑合能用"到"工程级可靠"。
本文将从底层原理到生产实战,逐个拆解 ES2026 的核心特性,配上大量可运行的代码示例,让你不只是"知道有这个东西",而是真正能在项目中用起来。
一、Temporal API:JavaScript 时间处理的终极答案
1.1 Date 对象的原罪
JavaScript 的 Date 对象自 1995 年诞生以来,一直是开发者痛苦的源泉。让我们先直面它的设计缺陷:
// 罪状一:月份从 0 开始,日从 1 开始——精神分裂
const date = new Date(2026, 4, 15); // 2026年5月15日,不是4月!
console.log(date.getMonth()); // 4,代表5月
// 罪状二:可变对象——函数参数传递后可能被意外修改
function formatDate(d) {
d.setHours(0, 0, 0, 0); // 副作用!修改了原始对象
return d.toISOString();
}
const myDate = new Date();
formatDate(myDate);
console.log(myDate); // 时间已经被改了!
// 罪状三:时区处理简直是灾难
const d = new Date('2026-05-15T10:00:00');
// 这个时间是 UTC 还是本地?取决于浏览器实现!
// 解析行为在不同环境下不一致
// 罪状四:没有纯日期、纯时间的类型
// 想表示"生日"?只能用 Date,但它总是带着时间
// 想表示"营业时间"?同样只能用 Date,但它总是带着日期
// 罪状五:日期运算需要手动计算毫秒
const oneWeekLater = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
// 但夏令时会让这个计算出错!
这些问题不是"用 moment.js 就好了"能解决的。第三方库能提供更好的 API,但无法解决 Date 对象本身在语言层面的设计缺陷。Temporal API 从根本上重新设计了时间处理模型。
1.2 Temporal 的设计哲学
Temporal 的核心理念可以概括为三点:不可变性、类型分离、时区显式化。
不可变性——所有 Temporal 对象一旦创建就不能修改,任何操作都返回新实例:
const now = Temporal.Now.zonedDateTimeISO('Asia/Shanghai');
const tomorrow = now.add({ days: 1 });
console.log(now.toString()); // 原对象没变
console.log(tomorrow.toString()); // 新对象
类型分离——不同场景用不同类型,不再用一个 Date 打天下:
| Temporal 类型 | 用途 | 示例 |
|---|---|---|
Temporal.PlainDate | 纯日期(生日、节假日) | 2026-05-15 |
Temporal.PlainTime | 纯时间(营业时间、闹钟) | 09:30:00 |
Temporal.PlainDateTime | 日期+时间(无时区) | 2026-05-15T09:30 |
Temporal.ZonedDateTime | 完整时间点(带时区) | 2026-05-15T09:30+08:00[Asia/Shanghai] |
Temporal.Instant | 绝对时间戳(UTC 纳秒) | 1747266600000000000n |
Temporal.Duration | 时间段 | P1DT2H30M(1天2小时30分) |
Temporal.PlainYearMonth | 年月(财年、学期) | 2026-05 |
Temporal.PlainMonthDay | 月日(每年重复的日期) | 05-15 |
时区显式化——没有默认时区的隐式转换,所有涉及时区的操作都必须明确指定:
// 创建带时区的时间
const shanghai = Temporal.Now.zonedDateTimeISO('Asia/Shanghai');
const ny = shanghai.withTimeZone('America/New_York');
console.log(shanghai.toString()); // 2026-05-29T02:02:00+08:00[Asia/Shanghai]
console.log(ny.toString()); // 2026-05-28T14:02:00-04:00[America/New_York]
// 同一时刻,不同时区表示,绝不混淆
1.3 PlainDate:再也不用担心月份从 0 开始
// 创建 PlainDate
const birthday = Temporal.PlainDate.from('2026-05-15');
const payday = new Temporal.PlainDate(2026, 5, 15); // 5就是5月!
console.log(birthday.year); // 2026
console.log(birthday.month); // 5(不是4!)
console.log(birthday.day); // 15
// 日期运算
const nextWeek = birthday.add({ weeks: 1 });
console.log(nextWeek.toString()); // 2026-05-22
const nextMonth = birthday.add({ months: 1 });
console.log(nextMonth.toString()); // 2026-06-15
// 溢出处理:1月31日 + 1个月 = ?
const jan31 = Temporal.PlainDate.from('2026-01-31');
const feb = jan31.add({ months: 1 });
console.log(feb.toString()); // 2026-02-28(自动收敛到月末,不会报错)
// 如果你想要不同的溢出行为
const febEnd = jan31.add({ months: 1 }, { overflow: 'reject' });
// RangeError: 不接受溢出——严格模式
// 日期差值
const start = Temporal.PlainDate.from('2026-01-01');
const end = Temporal.PlainDate.from('2026-05-15');
const diff = start.until(end);
console.log(diff.toString()); // P134D(134天)
console.log(diff.total('days')); // 134
console.log(diff.total('weeks')); // 19.14...
1.4 ZonedDateTime:正确处理时区和夏令时
这是 Temporal 最强大的类型,也是解决"跨时区 Bug"的终极武器:
// 从字符串创建(最推荐的方式)
const meeting = Temporal.ZonedDateTime.from(
'2026-03-08T10:00[America/New_York]'
);
// 上海同事看到的时间
const shanghai = meeting.withTimeZone('Asia/Shanghai');
console.log(shanghai.toString());
// 2026-03-08T23:00+08:00[Asia/Shanghai]
// 夏令时自动处理!
// 纽约 3月8日还在冬令时 (UTC-5)
// 纽约 3月15日已进入夏令时 (UTC-4)
const winterTime = Temporal.ZonedDateTime.from(
'2026-03-08T10:00[America/New_York]'
);
const summerTime = Temporal.ZonedDateTime.from(
'2026-03-15T10:00[America/New_York]'
);
console.log(winterTime.offsetNanoseconds / 3.6e12); // -5
console.log(summerTime.offsetNanoseconds / 3.6e12); // -4
// 同一本地时间,不同 UTC 偏移——Date 永远做不到这一点
1.5 Duration:时间段计算的革命
Temporal.Duration 解决了"30 天 ≠ 30×24 小时"这个反直觉但正确的问题:
// 创建 Duration
const oneMonth = Temporal.Duration.from({ months: 1 });
// 1月31日 + 1个月 = 2月28日
const jan31 = Temporal.PlainDate.from('2026-01-31');
const feb28 = jan31.add(oneMonth);
console.log(feb28.toString()); // 2026-02-28
// 但 2月28日 - 1月31日 ≠ 1个月
const back = feb28.subtract(oneMonth);
console.log(back.toString()); // 2026-01-28(不是1月31日!)
// 这就是日历运算的非对称性——Temporal 正确处理了它
// 30天的Duration在不同月份含义不同
const thirtyDays = Temporal.Duration.from({ days: 30 });
const oneMonthDur = Temporal.Duration.from({ months: 1 });
console.log(thirtyDays.equals(oneMonthDur)); // false!
// P30D ≠ P1M 因为日历月份天数不同
// 实际计算
const march1 = Temporal.PlainDate.from('2026-03-01');
console.log(march1.add(thirtyDays).toString()); // 2026-03-31
console.log(march1.add(oneMonthDur).toString()); // 2026-04-01
// 3月有31天,所以 +30天 和 +1个月 结果不同
// Duration 算术
const a = Temporal.Duration.from({ hours: 2, minutes: 30 });
const b = Temporal.Duration.from({ hours: 1, minutes: 45 });
const total = a.add(b);
console.log(total.toString()); // PT4H15M
// 总计换算
console.log(total.total('minutes')); // 255
console.log(total.total('hours')); // 4.25
1.6 实战:构建全球化日程系统
class GlobalSchedule {
constructor(timezone = 'Asia/Shanghai') {
this.timezone = timezone;
this.events = [];
}
// 添加事件:指定时间所在时区
addEvent(name, isoString) {
const zdt = Temporal.ZonedDateTime.from(isoString);
this.events.push({ name, time: zdt });
return this;
}
// 添加重复事件(每周)
addWeeklyEvent(name, startIso, weeks = 52) {
const start = Temporal.ZonedDateTime.from(startIso);
for (let i = 0; i < weeks; i++) {
this.events.push({
name,
time: start.add({ weeks: i })
});
}
return this;
}
// 按指定时区查看日程
viewIn(timezone) {
return this.events
.map(e => ({
name: e.name,
localTime: e.time.withTimeZone(timezone).toString(),
originalTime: e.time.toString()
}))
.sort((a, b) => a.localTime.localeCompare(b.localTime));
}
// 查找指定时间范围内的事件
findInRange(startIso, endIso, timezone) {
const tz = timezone || this.timezone;
const start = Temporal.ZonedDateTime.from(startIso).toInstant();
const end = Temporal.ZonedDateTime.from(endIso).toInstant();
return this.events.filter(e => {
const instant = e.time.toInstant();
return (
Temporal.Instant.compare(instant, start) >= 0 &&
Temporal.Instant.compare(instant, end) <= 0
);
});
}
}
// 使用示例
const schedule = new GlobalSchedule();
schedule
.addEvent('团队站会', '2026-06-01T10:00[Asia/Shanghai]')
.addEvent('客户演示', '2026-06-01T14:00[America/New_York]')
.addWeeklyEvent('1:1', '2026-06-02T15:00[Asia/Shanghai]', 4);
// 纽约同事查看
const nyView = schedule.viewIn('America/New_York');
console.log(nyView);
// 团队站会: 2026-06-01T22:00-04:00[America/New_York]
// 客户演示: 2026-06-01T14:00-04:00[America/New_York]
1.7 Temporal 与 Date 的迁移对照表
| 场景 | Date(旧) | Temporal(新) |
|---|---|---|
| 当前时间 | new Date() | Temporal.Now.zonedDateTimeISO() |
| 当前日期 | new Date().toLocaleDateString() | Temporal.Now.plainDateISO() |
| 解析 ISO | new Date(iso) (行为不一致) | Temporal.ZonedDateTime.from(iso) |
| 月份 | 0-11(要 +1) | 1-12(直觉正确) |
| 不可变 | ❌ 可变 | ✅ 所有操作返回新对象 |
| 时区 | 隐式本地/UTC | 显式指定,支持 IANA |
| 日期差 | 手动算毫秒 | until() 返回 Duration |
| 格式化 | toLocaleString() | toLocaleString() + 类型安全 |
1.8 Temporal 的性能考量
Temporal 对象创建成本比 Date 高(不可变 + 更丰富的内部状态),但在实际项目中,这几乎不构成瓶颈:
// 性能测试:创建100万个 Temporal 对象
console.time('Temporal.PlainDate');
for (let i = 0; i < 1_000_000; i++) {
Temporal.PlainDate.from('2026-05-15');
}
console.timeEnd('Temporal.PlainDate');
// 约 300-500ms(取决于引擎)
console.time('Date');
for (let i = 0; i < 1_000_000; i++) {
new Date('2026-05-15');
}
console.timeEnd('Date');
// 约 100-200ms
// 差距约 2-3x,但绝对值是微秒级的
// 对于正常的业务逻辑(每秒可能创建几十个),完全可以忽略
二、using / await using:确定性资源管理终于来了
2.1 问题:JavaScript 的资源泄漏困境
在 ES2026 之前,JavaScript 没有语言级别的资源释放机制。打开文件、数据库连接、网络 socket、GPU 上下文——这些资源的清理完全依赖开发者的自觉:
// 经典的 try-finally 模式(C/Java 时代的遗产)
async function readConfig(path) {
const file = await fs.open(path, 'r');
try {
const content = await file.readFile('utf-8');
return JSON.parse(content);
} finally {
await file.close(); // 希望没有人忘记写这行
}
}
// 问题一:如果 open 和 try 之间抛异常呢?
async function broken() {
const file = await fs.open(path, 'r');
doSomethingElse(); // 如果这里抛异常?file.close() 不会被调用
try {
// ...
} finally {
await file.close();
}
}
// 问题二:多个资源的嵌套——try-finally 地狱
async function multiResource() {
const conn = await pool.getConnection();
try {
const tx = await conn.beginTransaction();
try {
const lock = await acquireLock('key');
try {
// 终于到了业务逻辑
await doWork(conn, lock);
await tx.commit();
} finally {
await lock.release();
}
} finally {
await tx.rollback(); // 只有在没 commit 时才需要
}
} finally {
await conn.release();
}
}
Python 有 with,C# 有 using,Go 有 defer,Rust 有 RAII——几乎所有现代语言都有确定性资源释放。ES2026 终于给 JavaScript 补上了这一课。
2.2 Explicit Resource Management 提案详解
ES2026 引入了两个新关键字:using 和 await using,配合 Symbol.dispose 和 Symbol.asyncDispose 实现确定性资源管理。
核心概念:Disposable 协议
// 同步资源:实现 Symbol.dispose
class FileHandle {
#fd;
constructor(fd) {
this.#fd = fd;
}
read(size) {
return fs.readSync(this.#fd, Buffer.alloc(size));
}
[Symbol.dispose]() {
if (this.#fd !== undefined) {
fs.closeSync(this.#fd);
this.#fd = undefined;
}
}
}
// 异步资源:实现 Symbol.asyncDispose
class AsyncDatabaseConnection {
#pool;
#conn;
constructor(pool) {
this.#pool = pool;
}
async connect() {
this.#conn = await this.#pool.getConnection();
return this;
}
async query(sql) {
return this.#conn.query(sql);
}
async [Symbol.asyncDispose]() {
if (this.#conn) {
await this.#conn.release();
this.#conn = null;
}
}
}
2.3 using 的基本用法
// 同步 using——离开作用域时自动调用 [Symbol.dispose]()
{
using file = new FileHandle(fs.openSync('/path/to/file', 'r'));
const data = file.read(1024);
console.log(data);
// 离开这个块后,file[Symbol.dispose]() 自动被调用
} // 文件已关闭,即使上面抛了异常
// 即使抛异常也能正确清理
function safeRead() {
using file = new FileHandle(fs.openSync('/path/to/file', 'r'));
throw new Error('something went wrong');
// file[Symbol.dispose]() 仍然会被调用!
}
2.4 await using 的异步资源管理
// 异步 await using——等待 [Symbol.asyncDispose]() 完成
async function queryUser(id) {
await using db = new AsyncDatabaseConnection(pool);
await db.connect();
const result = await db.query(`SELECT * FROM users WHERE id = ${id}`);
return result;
// 离开函数作用域后,await db[Symbol.asyncDispose]() 自动被调用
// 注意:await using 会在清理时 await,确保异步资源正确释放
}
// 多个资源——不再需要 try-finally 嵌套
async function transferFunds(from, to, amount) {
await using conn = await AsyncDatabaseConnection.create(pool);
await using tx = await conn.beginTransaction();
await conn.query(`UPDATE accounts SET balance = balance - ${amount} WHERE id = ${from}`);
await conn.query(`UPDATE accounts SET balance = balance + ${amount} WHERE id = ${to}`);
await tx.commit();
// 释放顺序:先 tx,再 conn(与声明顺序相反,类似栈展开)
}
2.5 释放顺序与异常处理
// 释放顺序:后进先出(LIFO),与 C# using 和 Python with 一致
{
using a = createResource('A');
using b = createResource('B');
using c = createResource('C');
// 释放顺序:C → B → A
}
// 异常处理:即使 dispose 抛异常,也会继续释放其他资源
{
using a = createResource('A'); // a.dispose() 正常
using b = createResource('B'); // b.dispose() 抛异常
using c = createResource('C'); // c.dispose() 仍会被调用
// 最终抛出的是 b 的异常,a 和 c 的异常被记录为 SuppressedError
}
// SuppressedError:捕获被抑制的异常
try {
{
using outer = createFailingResource('outer');
using inner = createFailingResource('inner');
throw new Error('business logic failed');
}
} catch (e) {
console.log(e.message); // 'inner dispose failed'
console.log(e.suppressed.message); // 'business logic failed'
// e 是 SuppressedError,e.suppressed 是被抑制的原始异常
}
2.6 实战:构建可自动清理的测试框架
// 测试框架中 using 的妙用——自动清理测试副作用
class TestContext {
#cleanup = [];
// 创建临时文件,测试结束自动删除
async createTempFile(content) {
const path = `/tmp/test-${Date.now()}.tmp`;
await fs.writeFile(path, content);
this.#cleanup.push(() => fs.unlink(path));
return path;
}
// 启动测试服务器,测试结束自动关闭
async createTestServer(port) {
const server = await startServer(port);
this.#cleanup.push(() => server.close());
return server;
}
// mock 数据库,测试结束自动恢复
async mockDatabase(data) {
const original = await backupDatabase();
await seedDatabase(data);
this.#cleanup.push(() => restoreDatabase(original));
}
[Symbol.asyncDispose]() {
return Promise.all(this.#cleanup.map(fn => fn()));
}
}
// 使用:再也不怕测试污染
async function testUserCreation() {
await using ctx = new TestContext();
const server = await ctx.createTestServer(3000);
const db = await ctx.mockDatabase({ users: [] });
// 测试逻辑
const response = await fetch('http://localhost:3000/users', {
method: 'POST',
body: JSON.stringify({ name: 'test' })
});
assert(response.ok);
// 测试结束,临时文件、服务器、数据库全部自动清理
}
2.7 实战:GPU 上下文管理
// WebGPU 上下文——using 让资源管理变得优雅
class GPUContextManager {
#device;
#buffers = [];
static async create() {
const adapter = await navigator.gpu.requestAdapter();
const device = await adapter.requestDevice();
return new GPUContextManager(device);
}
constructor(device) {
this.#device = device;
}
createBuffer(size, usage) {
const buffer = this.#device.createBuffer({ size, usage });
this.#buffers.push(buffer);
return buffer;
}
createComputePipeline(descriptor) {
return this.#device.createComputePipeline(descriptor);
}
async [Symbol.asyncDispose]() {
// 确保所有 GPU 命令完成
await this.#device.queue.onSubmittedWorkDone();
// 销毁所有缓冲区
this.#buffers.forEach(b => b.destroy());
// 销毁设备
this.#device.destroy();
}
}
// 使用
async function gpuCompute() {
await using gpu = await GPUContextManager.create();
const inputBuffer = gpu.createBuffer(1024, GPUBufferUsage.STORAGE);
const outputBuffer = gpu.createBuffer(1024, GPUBufferUsage.STORAGE | GPUBufferUsage.MAP_READ);
// ... 执行计算 ...
// 无论如何退出,GPU 资源都会被正确清理
}
2.8 DisposableStack 和 AsyncDisposableStack
ES2026 还引入了两个工具类,用于管理动态资源:
// DisposableStack:动态注册同步清理函数
function processFiles(paths) {
using stack = new DisposableStack();
const handles = paths.map(path => {
const fd = fs.openSync(path, 'r');
stack.defer(() => fs.closeSync(fd)); // 动态注册清理
return fd;
});
// 也可以 use 一个已有的 Disposable 对象
const tempFile = stack.use(createTempFile());
// 处理文件...
return handles.map(fd => fs.readFileSync(fd, 'utf-8'));
// 离开作用域时,所有注册的清理函数按逆序执行
}
// AsyncDisposableStack:异步版本
async function migrateDatabase() {
await using stack = new AsyncDisposableStack();
const source = await stack.use(connectDatabase('source'));
const target = await stack.use(connectDatabase('target'));
stack.defer(async () => {
console.log('Migration completed, sending notification');
await sendNotification('migration-done');
});
await migrateData(source, target);
// 所有资源自动清理,通知也自动发送
}
// move():转移资源所有权
function createPersistentCache() {
using stack = new DisposableStack();
const conn = stack.use(openConnection());
const cache = new Cache(conn);
// 把清理责任转移给调用者
return stack.move(); // 返回一个新的 DisposableStack,由调用者负责释放
}
三、Set 方法家族:集合运算补齐了
3.1 JavaScript Set 的历史欠账
自从 ES6 引入 Set 以来,它一直是个"半成品"——只有基本的增删查,缺少数学集合运算。要做交集、并集、差集?自己写:
// ES2025 之前的痛点
const a = new Set([1, 2, 3]);
const b = new Set([2, 3, 4]);
// 交集——手动实现
const intersection = new Set([...a].filter(x => b.has(x))); // Set {2, 3}
// 并集——手动实现
const union = new Set([...a, ...b]); // Set {1, 2, 3, 4}
// 差集——手动实现
const difference = new Set([...a].filter(x => !b.has(x))); // Set {1}
// 对称差集——更复杂
const symmetricDifference = new Set([
...[...a].filter(x => !b.has(x)),
...[...b].filter(x => !a.has(x))
]); // Set {1, 4}
// 子集判断——手动实现
const isSubset = [...a].every(x => b.has(x));
这些实现不仅冗长,而且性能差——每次都要展开成数组再过滤。ES2026 一次性补齐了 7 个集合方法。
3.2 七大 Set 方法全解
const setA = new Set([1, 2, 3, 4]);
const setB = new Set([3, 4, 5, 6]);
// 1. union——并集
const union = setA.union(setB);
console.log(union); // Set {1, 2, 3, 4, 5, 6}
// 2. intersection——交集
const inter = setA.intersection(setB);
console.log(inter); // Set {3, 4}
// 3. difference——差集(A 有 B 没有的)
const diff = setA.difference(setB);
console.log(diff); // Set {1, 2}
// 4. symmetricDifference——对称差集(A|B 减去 A&B)
const symDiff = setA.symmetricDifference(setB);
console.log(symDiff); // Set {1, 2, 5, 6}
// 5. isSubsetOf——子集判断
const small = new Set([3, 4]);
console.log(small.isSubsetOf(setA)); // true
console.log(setA.isSubsetOf(small)); // false
// 6. isSupersetOf——超集判断
console.log(setA.isSupersetOf(small)); // true
console.log(small.isSupersetOf(setA)); // false
// 7. isDisjointFrom——不相交判断
const setC = new Set([7, 8, 9]);
console.log(setA.isDisjointFrom(setC)); // true(没有公共元素)
console.log(setA.isDisjointFrom(setB)); // false(有3、4公共元素)
3.3 性能优势:为什么原生方法比手动实现快
// 原生方法 vs 手动实现——100万元素基准测试
const bigA = new Set(Array.from({ length: 1_000_000 }, (_, i) => i));
const bigB = new Set(Array.from({ length: 1_000_000 }, (_, i) => i + 500_000));
// 手动交集
console.time('manual intersection');
const manual = new Set([...bigA].filter(x => bigB.has(x)));
console.timeEnd('manual intersection');
// ~400ms(需要展开成数组,创建100万个中间元素)
// 原生交集
console.time('native intersection');
const native = bigA.intersection(bigB);
console.timeEnd('native intersection');
// ~50ms(直接在 Set 内部迭代,无中间数组)
// 8x 性能提升!
原生方法为什么快?因为:
- 不需要中间数组:手动实现
[...setA].filter()先创建一个大数组,再遍历过滤 - 内部优化:引擎可以直接基于哈希表遍历,跳过不在另一个集合中的元素
- 内存友好:不创建中间数组,减少 GC 压力
3.4 实战:权限系统中的集合运算
class PermissionManager {
#roles = new Map();
#userRoles = new Map();
defineRole(name, permissions) {
this.#roles.set(name, new Set(permissions));
}
assignRole(userId, roleName) {
if (!this.#userRoles.has(userId)) {
this.#userRoles.set(userId, new Set());
}
this.#userRoles.get(userId).add(roleName);
}
// 获取用户的所有权限(角色的并集)
getUserPermissions(userId) {
const roles = this.#userRoles.get(userId) || new Set();
let permissions = new Set();
for (const role of roles) {
permissions = permissions.union(this.#roles.get(role) || new Set());
}
return permissions;
}
// 检查用户是否有指定权限
hasPermission(userId, permission) {
return this.getUserPermissions(userId).has(permission);
}
// 获取两个用户的共同权限(交集)
getSharedPermissions(userId1, userId2) {
return this.getUserPermissions(userId1)
.intersection(this.getUserPermissions(userId2));
}
// 获取 userId1 有但 userId2 没有的权限(差集)
getExclusivePermissions(userId1, userId2) {
return this.getUserPermissions(userId1)
.difference(this.getUserPermissions(userId2));
}
// 检查用户 A 的权限是否是用户 B 的子集
isPermissionSubsetOf(userId1, userId2) {
return this.getUserPermissions(userId1)
.isSubsetOf(this.getUserPermissions(userId2));
}
}
// 使用
const pm = new PermissionManager();
pm.defineRole('admin', ['read', 'write', 'delete', 'manage']);
pm.defineRole('editor', ['read', 'write']);
pm.defineRole('viewer', ['read']);
pm.assignRole('user1', 'admin');
pm.assignRole('user2', 'editor');
console.log(pm.getSharedPermissions('user1', 'user2'));
// Set {'read', 'write'}
console.log(pm.getExclusivePermissions('user1', 'user2'));
// Set {'delete', 'manage'}
console.log(pm.isPermissionSubsetOf('user2', 'user1'));
// true——editor 的权限是 admin 的子集
3.5 实战:数据同步中的集合运算
// 增量同步:找出新增、删除、不变的记录
function computeSyncDelta(localIds, remoteIds) {
const local = new Set(localIds);
const remote = new Set(remoteIds);
return {
toAdd: remote.difference(local), // 远程有本地没有→需要下载
toDelete: local.difference(remote), // 本地有远程没有→需要删除
unchanged: local.intersection(remote), // 两边都有→可能需要更新检查
allIds: local.union(remote), // 所有涉及到的 ID
isSynced: local.isDisjointFrom(remote.difference(local)) // 本地没有缺失的远程记录
};
}
// 示例
const localIds = [1, 2, 3, 5, 8];
const remoteIds = [2, 3, 4, 5, 6, 7];
const delta = computeSyncDelta(localIds, remoteIds);
console.log(delta.toAdd); // Set {4, 6, 7}
console.log(delta.toDelete); // Set {1, 8}
console.log(delta.unchanged); // Set {2, 3, 5}
四、Promise.try:同步异常不再逃离异步边界
4.1 问题:Promise 构造函数中的异常陷阱
// 看起来安全的异步代码,其实有隐患
async function fetchUserData(userId) {
// 如果 validateUserId 抛同步异常,会发生什么?
validateUserId(userId); // 同步验证
const response = await fetch(`/api/users/${userId}`);
return response.json();
}
// 调用方用 .catch() 捕获错误
fetchUserData('invalid').catch(err => {
// 如果 validateUserId 抛异常,这个 catch 能捕获吗?
// 答案:能,因为 async 函数自动将同步异常转为 rejected Promise
});
// 但如果不用 async 函数呢?
function fetchUserData2(userId) {
validateUserId(userId); // 同步异常!
return fetch(`/api/users/${userId}`)
.then(res => res.json());
}
// 调用方
try {
fetchUserData2('invalid'); // 同步异常在这里抛出!
} catch (err) {
// 能捕获,但这是同步异常,不是 Promise rejection
}
fetchUserData2('invalid').catch(err => {
// 永远不会到这里——异常已经在上面同步抛出了
});
这种"有时是同步异常,有时是异步 rejection"的不一致性,是 JavaScript 异步编程中最常见的 Bug 来源之一。
4.2 Promise.try 的解决方案
// Promise.try:将可能同步抛异常的代码包装成 Promise
const result = Promise.try(() => {
validateUserId(userId); // 如果抛异常,变成 rejected Promise
return fetch(`/api/users/${userId}`);
});
result.catch(err => {
// 无论是同步异常还是异步 rejection,都能在这里捕获
console.error('Failed:', err);
});
// 重写上面的例子
function fetchUserData3(userId) {
return Promise.try(() => {
validateUserId(userId); // 同步异常 → rejected Promise
return fetch(`/api/users/${userId}`);
})
.then(res => res.json());
}
// 现在错误处理是一致的了
fetchUserData3('invalid').catch(err => {
// 一定能捕获到,无论是同步还是异步错误
});
4.3 Promise.try vs new Promise 包装
// 方案一:new Promise 包装(冗长且容易出错)
function fetchUserDataOld(userId) {
return new Promise((resolve, reject) => {
try {
validateUserId(userId);
resolve(fetch(`/api/users/${userId}`));
} catch (err) {
reject(err);
}
}).then(res => res.json());
}
// 方案二:Promise.try(简洁清晰)
function fetchUserDataNew(userId) {
return Promise.try(() => {
validateUserId(userId);
return fetch(`/api/users/${userId}`);
}).then(res => res.json());
}
// 方案三:async/await(也可以,但 Promise.try 更轻量)
async function fetchUserDataAsync(userId) {
validateUserId(userId); // async 函数自动将异常转为 rejection
const res = await fetch(`/api/users/${userId}`);
return res.json();
}
4.4 实战:统一错误处理中间件
// Express 风格的异步错误处理中间件
function asyncHandler(fn) {
return (req, res, next) => {
Promise.try(() => fn(req, res, next))
.catch(next); // 所有错误(同步+异步)统一交给错误处理中间件
};
}
// 使用
app.get('/users/:id', asyncHandler(async (req, res) => {
// 这里无论同步异常还是异步 rejection,都能被正确捕获
const userId = req.params.id;
if (!isValidId(userId)) {
throw new ValidationError('Invalid user ID'); // 同步异常
}
const user = await db.findUser(userId); // 异步 rejection
if (!user) {
throw new NotFoundError('User not found'); // 同步异常
}
res.json(user);
}));
// 错误处理中间件
app.use((err, req, res, next) => {
if (err instanceof ValidationError) {
res.status(400).json({ error: err.message });
} else if (err instanceof NotFoundError) {
res.status(404).json({ error: err.message });
} else {
res.status(500).json({ error: 'Internal Server Error' });
}
});
五、Iterator Helpers:迭代器的链式操作
5.1 现状:迭代器的痛苦
Generator 和 Iterator 是 ES6 引入的强大特性,但它们的操作方式一直很原始——没有 map、filter、reduce,只能手动 for-of 或展开成数组:
// 当前:要操作迭代器,必须先转成数组
function* naturalNumbers() {
for (let i = 1; ; i++) yield i;
}
// 想取前10个偶数的平方?要么这样:
const squares = [...naturalNumbers()]
.filter(n => n % 2 === 0)
.map(n => n ** 2)
.slice(0, 10);
// 问题:naturalNumbers() 是无限的![...] 会无限循环崩溃!
5.2 Iterator Helpers 方法列表
ES2026 给每个 Iterator/Generator 实例添加了以下方法:
// .map(fn)——转换每个元素
// .filter(fn)——过滤元素
// .take(n)——取前 n 个元素
// .drop(n)——跳过前 n 个元素
// .flatMap(fn)——展平映射
// .reduce(fn, initial)——归约
// .toArray()——转为数组
// .forEach(fn)——遍历
// .some(fn)——存在满足条件的元素
// .every(fn)——所有元素都满足条件
// .find(fn)——查找第一个满足条件的元素
// .from(iterable)——静态方法,将任意可迭代对象转为 Iterator
5.3 惰性求值:Iterator Helpers 的核心优势
// 无限序列 + 链式操作 = 惰性求值
function* naturalNumbers() {
for (let i = 1; ; i++) yield i;
}
const result = naturalNumbers()
.filter(n => n % 2 === 0) // 只取偶数
.map(n => n ** 2) // 平方
.take(10) // 只取10个
.toArray(); // 触发求值
console.log(result); // [4, 16, 36, 64, 100, 144, 196, 256, 324, 400]
// 关键:惰性求值!
// 1. filter 不需要遍历所有自然数——只要产出足够的偶数
// 2. map 也不需要处理所有偶数——只要处理被 take 采纳的
// 3. take(10) 满足后立即停止迭代
// 整个过程只迭代到 20(第10个偶数),不会无限循环
5.4 性能对比:Iterator Helpers vs 数组方法
// 场景:从1亿个数字中找出前5个满足条件的
const hugeArray = Array.from({ length: 100_000_000 }, (_, i) => i + 1);
// 方法一:数组方法(需要处理全部1亿个元素)
console.time('array methods');
const arrayResult = hugeArray
.filter(n => n % 3 === 0)
.map(n => n * 2)
.slice(0, 5);
console.timeEnd('array methods');
// ~3000ms + 大量内存
// 方法二:Iterator Helpers(惰性求值,只处理到满足条件为止)
console.time('iterator helpers');
function* generateNumbers(max) {
for (let i = 1; i <= max; i++) yield i;
}
const iterResult = generateNumbers(100_000_000)
.filter(n => n % 3 === 0)
.map(n => n * 2)
.take(5)
.toArray();
console.timeEnd('iterator helpers');
// ~1ms —— 因为只迭代到 9(第5个3的倍数是15,×2=30)
// 3000x 性能差距!
5.5 实战:日志流处理
// 模拟无限日志流
function* logStream() {
const levels = ['INFO', 'WARN', 'ERROR', 'DEBUG', 'INFO'];
let i = 0;
while (true) {
yield {
level: levels[i % levels.length],
message: `Log entry ${i}`,
timestamp: Date.now()
};
i++;
}
}
// 用 Iterator Helpers 构建实时日志管道
const errorLogs = logStream()
.filter(log => log.level === 'ERROR' || log.level === 'WARN')
.map(log => `[${log.level}] ${log.message}`)
.take(20)
.toArray();
console.log(errorLogs);
// 更复杂的管道
function processLogs(source) {
return source
.drop(10) // 跳过启动日志
.filter(log => log.level !== 'DEBUG') // 过滤调试日志
.map(log => ({ // 转换格式
severity: log.level === 'ERROR' ? 'critical' : 'normal',
text: log.message,
time: new Date(log.timestamp).toISOString()
}))
.take(100) // 只处理100条
.reduce((acc, log) => { // 统计
acc[log.severity] = (acc[log.severity] || 0) + 1;
return acc;
}, {});
}
const stats = processLogs(logStream());
console.log(stats); // { critical: N, normal: M }
六、ES2026 其他值得关注的特性
6.1 Error.isError:可靠的错误类型判断
// 问题:跨 realm 的 instanceof 失败
// 比如 iframe 中的 Error 对象
const iframe = document.createElement('iframe');
document.body.appendChild(iframe);
const iframeError = new iframe.contentWindow.Error('test');
console.log(iframeError instanceof Error); // false!
// 因为 iframe 有自己的 Error 构造函数
// Error.isError 解决这个问题
console.log(Error.isError(iframeError)); // true
console.log(Error.isError(new Error())); // true
console.log(Error.isError({})); // false
console.log(Error.isError(null)); // false
// 实际应用:统一错误处理
function handleError(err) {
if (Error.isError(err)) {
console.error(`[${err.name}] ${err.message}`);
if (err.stack) console.error(err.stack);
} else {
console.error('Unknown error:', err);
}
}
6.2 Math.sumPrecise:精确的数组求和
// 问题:浮点数累加误差
const numbers = [0.1, 0.2, 0.3, 0.4, 0.5];
let sum = 0;
for (const n of numbers) {
sum += n;
}
console.log(sum); // 1.5000000000000002(浮点误差!)
console.log(Math.sumPrecise(numbers)); // 1.5(精确!)
// 原理:使用 Kahn summation 或类似的补偿算法
// 在底层用更高精度计算,避免中间步骤的截断误差
// 大数组场景更明显
const bigArray = Array.from({ length: 10_000_000 }, () => 0.1);
let nativeSum = 0;
for (const n of bigArray) nativeSum += n;
console.log(nativeSum); // 1000000.000012(误差12)
console.log(Math.sumPrecise(bigArray)); // 1000000(精确)
七、兼容性与迁移策略
7.1 浏览器支持现状(2026年5月)
| 特性 | Chrome | Firefox | Safari | Node.js |
|---|---|---|---|---|
| Temporal | 134+ ✅ | 135+ ✅ | 18.2+ ✅ | 22+ ✅ |
| using/await using | 134+ ✅ | 135+ ✅ | 18.2+ ✅ | 22+ ✅ |
| Set 方法 | 132+ ✅ | 134+ ✅ | 17.4+ ✅ | 22+ ✅ |
| Promise.try | 133+ ✅ | 134+ ✅ | 17.4+ ✅ | 22+ ✅ |
| Iterator Helpers | 133+ ✅ | 135+ ✅ | 17.4+ ✅ | 22+ ✅ |
| Error.isError | 135+ ✅ | 135+ ✅ | 18.2+ ✅ | 23+ ✅ |
| Math.sumPrecise | 134+ ✅ | 135+ ✅ | 18.2+ ✅ | 22+ ✅ |
7.2 Polyfill 策略
// Temporal polyfill(使用 @js-temporal/polyfill)
import { Temporal } from '@js-temporal/polyfill';
if (!globalThis.Temporal) {
globalThis.Temporal = Temporal;
}
// Set 方法 polyfill
if (!Set.prototype.union) {
Set.prototype.union = function(other) {
const result = new Set(this);
for (const item of other) result.add(item);
return result;
};
Set.prototype.intersection = function(other) {
const result = new Set();
for (const item of this) {
if (other.has(item)) result.add(item);
}
return result;
};
Set.prototype.difference = function(other) {
const result = new Set(this);
for (const item of other) result.delete(item);
return result;
};
Set.prototype.symmetricDifference = function(other) {
const result = new Set();
for (const item of this) {
if (!other.has(item)) result.add(item);
}
for (const item of other) {
if (!this.has(item)) result.add(item);
}
return result;
};
Set.prototype.isSubsetOf = function(other) {
for (const item of this) {
if (!other.has(item)) return false;
}
return true;
};
Set.prototype.isSupersetOf = function(other) {
return other.isSubsetOf(this);
};
Set.prototype.isDisjointFrom = function(other) {
for (const item of this) {
if (other.has(item)) return false;
}
return true;
};
}
// Promise.try polyfill
if (!Promise.try) {
Promise.try = function(fn, ...args) {
return new Promise(resolve => resolve(fn(...args)));
};
}
7.3 渐进迁移方案
// 第一步:在新代码中使用 ES2026 特性,旧代码不动
// 新的日期处理——全部用 Temporal
function formatDate(date) {
// 新代码
const plainDate = Temporal.PlainDate.from(date);
return plainDate.toLocaleString('zh-CN');
}
// 第二步:统一入口——适配新旧 API
function createDate(input) {
// 尝试 Temporal
try {
return Temporal.PlainDate.from(input);
} catch {
// 回退到 Date
return new Date(input);
}
}
// 第三步:using 在新模块中优先使用
// 数据库模块
export async function withConnection(fn) {
await using conn = await createConnection();
return fn(conn);
// 连接自动释放
}
八、总结与展望
ES2026 是 JavaScript 近十年来最重磅的版本更新。让我们回顾一下核心变化:
| 特性 | 解决的核心问题 | 影响等级 |
|---|---|---|
| Temporal API | Date 对象 29 年的设计缺陷 | ⭐⭐⭐⭐⭐ 革命性 |
| using / await using | 确定性资源管理缺失 | ⭐⭐⭐⭐ 重大 |
| Set 方法 | 集合运算原语缺失 | ⭐⭐⭐ 实用 |
| Promise.try | 同步/异步异常不一致 | ⭐⭐⭐ 实用 |
| Iterator Helpers | 迭代器缺乏链式操作 | ⭐⭐⭐⭐ 重大 |
| Error.isError | 跨 realm 错误判断 | ⭐⭐ 边缘场景 |
| Math.sumPrecise | 浮点累加误差 | ⭐⭐ 特定场景 |
我的判断:
Temporal 将在未来 2-3 年内全面替代
Date。就像async/await替代回调一样,这不是"可选项",而是"必然趋势"。任何涉及时间处理的新代码,都应该从第一天起使用 Temporal。using 的影响可能比很多人预期的更大。它不只是"语法糖"——它改变了代码的组织方式。当资源清理变成自动的,你会写出更安全的代码,更少的 try-finally,更少的资源泄漏 Bug。Node.js 22+ 的
fs.open()返回的FileHandle已经实现了Symbol.asyncDispose,浏览器端的 WebGPU、IndexedDB 等接口也在跟进。Set 方法 和 Iterator Helpers 是"用了就回不去"的体验提升。它们的真正价值不在于单独的 API,而在于让 JavaScript 的数据操作范式更加一致——数组有
map/filter,Set 有union/intersection,Iterator 有惰性链——三种数据结构各有各的惯用操作,不再需要互相转换。Promise.try 看起来微小,但解决的是异步编程中最阴险的一类 Bug。推荐在所有可能抛同步异常的 Promise 链入口处使用。
JavaScript 正在从"凑合能用"走向"工程级可靠"。ES2026 的每一个特性都在朝这个方向推进。作为开发者,我们能做的最好的事就是:尽早了解,渐进采用,让新特性为代码质量服务,而不是为用而用。
附录:ES2026 特性速查表
// === Temporal ===
Temporal.Now.zonedDateTimeISO() // 当前时区时间
Temporal.Now.plainDateISO() // 当前日期
Temporal.Now.plainTimeISO() // 当前时间
Temporal.Now.instant() // 当前 UTC 时间戳
Temporal.PlainDate.from('2026-05-15')
Temporal.ZonedDateTime.from('2026-05-15T10:00[Asia/Shanghai]')
Temporal.Duration.from({ hours: 2, minutes: 30 })
// === using / await using ===
using resource = createSyncDisposable();
await using asyncResource = createAsyncDisposable();
new DisposableStack()
new AsyncDisposableStack()
// === Set 方法 ===
setA.union(setB)
setA.intersection(setB)
setA.difference(setB)
setA.symmetricDifference(setB)
setA.isSubsetOf(setB)
setA.isSupersetOf(setB)
setA.isDisjointFrom(setB)
// === Promise.try ===
Promise.try(() => mightThrowSync())
.then(result => handleResult(result))
.catch(err => handleError(err));
// === Iterator Helpers ===
iterator.map(fn).filter(fn).take(n).drop(n).toArray()
iterator.flatMap(fn).reduce(fn, init).forEach(fn)
iterator.some(fn).every(fn).find(fn)
Iterator.from(iterable)
// === Error.isError ===
Error.isError(value) // true/false
// === Math.sumPrecise ===
Math.sumPrecise([0.1, 0.2, 0.3]) // 0.6(精确)