编程 ECMAScript 2026 正式获批:深度拆解 17 个新特性与开发实战指南

2026-07-11 05:14:52 +0800 CST views 8

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 基础设施的构建者,这些新特性都可能重塑你未来几年的编码方式。


一、usingawait 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]()

这里有一个关键细节:usingawait using 是词法级别的。无论你是 returnthrow、还是代码走到了作用域末尾,资源都会被正确清理。这一点比 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 当且仅当:

  1. 该值是原生的 Error 对象(ErrorTypeErrorRangeError 等)
  2. 或者该值是任何通过 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 之一:

  1. 月份从 0 开始:令人困惑的 off-by-one 错误
  2. 没有不可变版本:所有方法都修改原对象
  3. 时区操作极复杂:需要手动处理 offset
  4. 字符串解析不可靠new Date('2026-06-30') 的行为因环境而异
  5. 没有日期计算标准 API:加减天数需要手动计算毫秒
  6. 缺少年月日单独表示:只有 DateTime,没有 Date 或 Time 的独立类型
  7. 夏令时处理混乱:跨时区计算容易出错

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 的内置方法(unionintersection 等),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 各特性的落地时间表

特性规范状态ChromeFirefoxSafariNode.js
using/await usingStage 3115+支持15.4+20+
Error.isError()Stage 3118+支持15.4+20+
Array.fromAsync()Stage 3110+支持16.0+20+
Math.sumPrecise()Stage 3实现中实现中实现中22+
Iterator.concat()Stage 3122+支持16.4+20+
Map/WeakMap 新方法Stage 3实现中实现中实现中22+
Uint8Array 编码Stage 3实现中实现中实现中22+
JSON.rawJSON()Stage 3实现中实现中实现中22+
Records & TuplesStage 3122+ (实验)实现中实现中22+
Pattern MatchingStage 3119+ (实验)实现中实现中22+
Temporal APIStage 3122+ (实验)实现中实现中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 语言委员会对生产级可靠性的追求。语言正在从"能用"向"好用"和"可靠"演进——这不是最激动人心的语言升级,但可能是对日常开发者影响最深远的版本之一。

作为开发者,我们应该:

  1. 立即学习 Error.isError()Array.fromAsync()、Set 新方法——这些已经稳定
  2. 密切关注 using/await using 的生态迁移——Node.js 核心 API 正在适配
  3. 开始实验 Records & Tuples、Pattern Matching、Temporal API——为未来做准备
  4. 重构旧代码 用新 API 替代手写的 workaround——代码更简洁,也更可靠

JavaScript 的下一个十年,将是这些现代特性全面落地的十年。拥抱 ES2026,从现在开始。


参考链接:

复制全文 生成海报 ES2026 JavaScript ECMAScript Temporal 前端开发

推荐文章

Vue3中如何处理跨域请求?
2024-11-19 08:43:14 +0800 CST
CentOS 镜像源配置
2024-11-18 11:28:06 +0800 CST
Vue3中如何处理状态管理?
2024-11-17 07:13:45 +0800 CST
go发送邮件代码
2024-11-18 18:30:31 +0800 CST
程序员茄子在线接单