编程 ES2026 深度实战:Temporal 终结 Date 时代、using 资源管理与 Set 家族全面进化——JavaScript 十年来最大语法升级完全指南

2026-05-29 02:07:11 +0800 CST views 10

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()
解析 ISOnew 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 引入了两个新关键字:usingawait using,配合 Symbol.disposeSymbol.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 性能提升!

原生方法为什么快?因为:

  1. 不需要中间数组:手动实现 [...setA].filter() 先创建一个大数组,再遍历过滤
  2. 内部优化:引擎可以直接基于哈希表遍历,跳过不在另一个集合中的元素
  3. 内存友好:不创建中间数组,减少 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 引入的强大特性,但它们的操作方式一直很原始——没有 mapfilterreduce,只能手动 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月)

特性ChromeFirefoxSafariNode.js
Temporal134+ ✅135+ ✅18.2+ ✅22+ ✅
using/await using134+ ✅135+ ✅18.2+ ✅22+ ✅
Set 方法132+ ✅134+ ✅17.4+ ✅22+ ✅
Promise.try133+ ✅134+ ✅17.4+ ✅22+ ✅
Iterator Helpers133+ ✅135+ ✅17.4+ ✅22+ ✅
Error.isError135+ ✅135+ ✅18.2+ ✅23+ ✅
Math.sumPrecise134+ ✅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 APIDate 对象 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(精确)
复制全文 生成海报 ES2026 JavaScript Temporal using Set

推荐文章

2025,重新认识 HTML!
2025-02-07 14:40:00 +0800 CST
任务管理工具的HTML
2025-01-20 22:36:11 +0800 CST
Go 如何做好缓存
2024-11-18 13:33:37 +0800 CST
PHP解决XSS攻击
2024-11-19 02:17:37 +0800 CST
Vue中如何处理异步更新DOM?
2024-11-18 22:38:53 +0800 CST
虚拟DOM渲染器的内部机制
2024-11-19 06:49:23 +0800 CST
Vue 3 中的 Watch 实现及最佳实践
2024-11-18 22:18:40 +0800 CST
平面设计常用尺寸
2024-11-19 02:20:22 +0800 CST
如何在Rust中使用UUID?
2024-11-19 06:10:59 +0800 CST
程序员茄子在线接单