🔥 ECMAScript 2026 来了!使用这些新特性,JS 代码直接少一半

前言

Hello~大家好,我是秋天的一阵风

2026 年 6 月 30 日,Ecma International 正式拍板了 ECMAScript 2026 语言规范。这是 JavaScript 的第 17 个版本。

回头看挺有意思的------十年前 ES6 给 JS 加了 class、箭头函数、Promise,当时大家觉得"JS 终于像门正经语言了"。十年后的 ES2026,TC39 这帮人没搞什么炫技的新语法,而是老老实实把开发者吐槽了十年的坑一个个填上了。

💡 拓展阅读:什么是 TC39

TC39 全称 Technical Committee 39,是 Ecma International 下设的官方技术委员会⚙️,全权负责 ECMAScript(JavaScript)语言规范的设计、评审与迭代。成员包含各大浏览器厂商、前端企业与行业专家👨‍💻,所有语言新特性都要经过完整 Stage 0~Stage 4 阶段提案流程📑,走完 Stage 4 才能正式纳入标准。

🔗 官方资源直达:

  • 📌 TC39 官方站点(提案进度、标准流程):tc39.es/
  • 📂 GitHub 开源仓库(全部提案、ECMA-262 规范源码):github.com/tc39

一、using 关键字:你的资源会自动释放了

以前有多痛苦

写过 Node.js 的人一定见过这种代码:

javascript 复制代码
// 旧写法:try-finally 套娃
async function processFile(path) {
  let fileHandle;
  let dbConn;
  try {
    fileHandle = await fs.open(path, 'r');
    dbConn = await pool.getConnection();
    const data = await fileHandle.read();
    await dbConn.query('INSERT INTO logs VALUES (?)', [data]);
    return processData(data);
  } finally {
    if (fileHandle) await fileHandle.close();
    if (dbConn) await dbConn.end();
  }
}

两个资源就这么啰嗦了。真实项目里文件句柄、数据库连接、网络请求、分布式锁......四五个资源套在一起,finally 块比业务逻辑还长,忘了关一个就是内存泄漏的生产事故。

ES2026 怎么解决的

javascript 复制代码
// ES2026:using 关键字,离开作用域自动清理
async function processFile(path) {
  await using fileHandle = await fs.open(path, 'r');
  await using dbConn = await pool.getConnection();

  const data = await fileHandle.read();
  await dbConn.query('INSERT INTO logs VALUES (?)', [data]);
  return processData(data);
} // ← 大括号结束,fileHandle 和 dbConn 自动关闭

using 的作用就一句话:声明在哪个大括号里,离开那个大括号的时候就自动释放 。不管是 returnthrow 还是正常执行完,它都会帮你收尾。

原理长什么样

任意对象只要实现 [Symbol.dispose]()(同步)或 [Symbol.asyncDispose]()(异步),就能配合 using

javascript 复制代码
class FileHandle {
  #fd;

  constructor(path) {
    this.#fd = fs.openSync(path, 'r');
  }

  read() {
    return fs.readFileSync(this.#fd, 'utf-8');
  }

  [Symbol.dispose]() {
    // using 块结束时自动走到这里
    fs.closeSync(this.#fd);
    console.log('文件已关闭');
  }
}

// 用起来干净利落
{
  using handle = new FileHandle('./config.json');
  const content = handle.read();
  // do something...
} // ← 自动调用 [Symbol.dispose]()

多个资源时,释放顺序是栈式的

后声明的先释放,像叠盘子一样:

javascript 复制代码
async function uploadAndNotify(filePath, recipients) {
  await using stream = fs.createReadStream(filePath);  // 第三个释放
  await using conn = await smtp.connect('smtp.example.com'); // 第二个释放
  await using session = await conn.login(credentials); // 第一个释放

  // 上传文件流...
  // 发送通知邮件...
}
// 不管中间哪里报错,释放顺序永远是:session → conn → stream
// 不会出现"连接断了但文件句柄还开着"的情况

另外,如果业务逻辑抛异常的同时 dispose() 自己也失败了,ES2026 会把 dispose 的错误打包成 SuppressedError 附加在主错误上------两个都不会静默丢失。

二、Error.isError():终于不用 instanceof Error

判断一个东西是不是 Error,在 JS 里一直都挺玄学的:

javascript 复制代码
// 旧写法的问题
try {
  riskyOperation();
} catch (e) {
  if (e instanceof Error) {
    // ❌ 看起来没问题,但你猜怎么着------有时候是 false
  }
}

问题出在哪?JS 的"世界"不止一个 。浏览器里每开一个 tab、每嵌一个 iframe、每启一个 Web Worker,内部都是一个独立的小宇宙,各自有一套完整的 ErrorTypeErrorRangeError 构造函数。

想象一下:iframe A 里抛了个 TypeError,你的主页面 try-catch 抓住了它,然后用 instanceof Error 去判断------结果 false。因为它是 iframe 那个世界里的 Error,跟主页面你手里这个 Error 虽然长得一模一样,但根本不是同一个构造函数。

javascript 复制代码
// 举个能跑的栗子 🌰
// 页面里塞了一个 iframe,从 iframe 拿 Error
const iframe = document.createElement('iframe');
document.body.appendChild(iframe);

const iframeError = new iframe.contentWindow.Error('来自 iframe 的错误');
console.log(iframeError instanceof Error);  // false 😱
// 明明是个 Error,instanceof 却说不是

Error.isError() 就是来收拾这个烂摊子的------它不看"你是哪个世界的 Error",只看"你是不是个 Error":

javascript 复制代码
// ES2026:直接问"你是不是 Error?"不看户口本
try {
  throw new TypeError('出错了');
} catch (e) {
  console.log(Error.isError(e));  // true ✅
}

// 来自 iframe 的错误也能认出来
console.log(Error.isError(iframeError));    // true ✅

// 字符串、对象、null 统统正确返回 false
console.log(Error.isError('just a string'));   // false ✅
console.log(Error.isError({ message: 'x' }));   // false ✅
console.log(Error.isError(null));               // false ✅

就一行 API,但把 instanceof Error 这个坑了前端十年的"跨世界认证问题"给解决了。

三、Math.sumPrecise():大量数字求和再也不丢精度了

先说清楚------它不能解决 0.1 + 0.2 的问题:

javascript 复制代码
Math.sumPrecise([0.1, 0.2]);  // 还是 0.30000000000000004 😅

为什么?因为 0.1 和 0.2 从被写出来的那一刻,在 IEEE 754 里就已经是不精确的近似值了。"表示误差"发生在加法之前,Kahan 算法管不到。

那它到底解决什么?累加误差。当你把一大堆浮点数串行相加时,每次加法都会产生微小的舍入误差。少量数字没关系,几十上百个加起来,误差就会累积到肉眼可见。

javascript 复制代码
// 大量数字累加:reduce 翻车,sumPrecise 稳住
const prices = Array.from({ length: 1000 }, (_, i) => 0.01 * (i + 1));

const naive = prices.reduce((a, b) => a + b, 0);
const precise = Math.sumPrecise(prices);

console.log(naive === precise);  // false,reduce 有累积误差
console.log(precise);            // 正确值

最经典的演示是这个:

javascript 复制代码
const tricky = [1e20, 0.1, -1e20];

console.log(tricky.reduce((a, b) => a + b, 0));  // 0 ❌ 那个 0.1 直接被吞了
console.log(Math.sumPrecise(tricky));             // 0.1 ✅

普通累加时,1e20 + 0.1 的结果约等于 1e20(0.1 的精度被巨大的数量级差吃掉了),再 - 1e20 就只剩 0。Kahan-Neumaier 算法在每一步加法时记录被丢弃的尾数,最后补回来。

一句话总结Math.sumPrecise() 修的是"加很多次"积累的误差,不是"0.1 本身就不精确"这个先天问题。如果你需要任意精度的十进制运算(比如银行利息计算),该用 BigInt 或 decimal 库还是得用。

四、Array.fromAsync():异步迭代器的归途

Array.from() 大家天天用------把同步迭代器转成数组。但同名的异步版本一直没有:

javascript 复制代码
// 旧写法:遍历异步迭代器,手动 push
const results = [];
for await (const item of asyncGenerator()) {
  results.push(await processItem(item));
}

现在有了 Array.fromAsync()

javascript 复制代码
// ES2026:从异步数据源直接生成数组
const files = await Array.fromAsync(
  getAsyncFilenames(),
  async (name) => await readFile(name, 'utf-8')
);
// 按顺序返回,无论并发度如何

// 也支持同步 generator 产 Promise
function* gen() {
  yield fetch('/api/users/1').then(r => r.json());
  yield fetch('/api/users/2').then(r => r.json());
}
const users = await Array.fromAsync(gen());

本质就是把异步迭代器里的东西一个个取出来,放进数组里返回。第二个参数是个可选的映射函数,和 Array.from() 的用法一脉相承。

五、Uint8Array 原生支持 Base64 / Hex 编解码

前端跟二进制数据打交道时,Base64 和十六进制转换几乎绕不开。以前得自己写或者拉一个库:

javascript 复制代码
// 旧方法:btoa / atob,而且只支持 Latin-1 字符
const text = 'Hello World';
const base64 = btoa(text);  // 碰到中文就不行了

// 十六进制转换还要自己写循环
function bytesToHex(bytes) {
  return Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
}

ES2026 直接在 Uint8Array 上提供了原生方法:

javascript 复制代码
// 编码
const encoder = new TextEncoder();
const data = encoder.encode('Hello, ES2026! 😎');

const base64 = data.toBase64();          // "SGVsbG8sIEVTMjAyNiEg8J+Yjg=="
const hex = data.toHex();                // "48656c6c6f2c204553323032362120f09f988e"

// 还支持 URL-safe Base64
const urlSafe = data.toBase64({ alphabet: 'base64url' });

// 解码也很简单
const fromB64 = Uint8Array.fromBase64(base64);
const fromHex = Uint8Array.fromHex(hex);

console.log(new TextDecoder().decode(fromB64));  // "Hello, ES2026! 😎"

以后处理图片上传、哈希值展示、URL 编码这些场景,不用再 npm install base64-js 了。

六、Iterator.concat():迭代器拼接,不做数组展开

很多时候你手上有好几个数据源想把它们串起来遍历。以前的做法是展开成数组再拼:

javascript 复制代码
// 旧方法:把迭代器全部展开到内存里再合并
const combined = [...dataSet1, ...dataSet2, ...dataSet3];
// 三个数据集如果都有 10 万条记录,内存直接炸了

Iterator.concat() 返回的是一个惰性迭代器,不一次性展开:

javascript 复制代码
// ES2026:惰性拼接,O(1) 内存
const i1 = fetchLargeDataset('table1');
const i2 = fetchLargeDataset('table2');
const i3 = ['fallback-item'];

const combined = Iterator.concat(i1, i2, i3);

// 遍历时才逐个消费,不会撑爆内存
for (const row of combined) {
  processRow(row);
}

// 中间可以混入普通值
const months = Iterator.concat(
  Iterator.from([2022, 2023]),
  [2024],                         // 普通数组也能插入
  Iterator.from([2025, 2026])
);
console.log(Array.from(months));  // [2022, 2023, 2024, 2025, 2026]

对于处理大文件流、分页数据、多个 API 数据源的场景特别有用------避免了"把好几个大数据集全部塞进内存再一起处理"的浪费。

七、Map.getOrInsert():一行消灭 if-has-set-get

几乎每个项目里都有这样的代码:

javascript 复制代码
const cache = new Map();

// 老写法:先检查有没有,没有就塞进去,再取出来
function getCached(key, computeFn) {
  if (!cache.has(key)) {
    cache.set(key, computeFn());
  }
  return cache.get(key);
}

这四行模板代码在 Map 的使用场景里反复出现。ES2026 直接把它压缩成一拍:

javascript 复制代码
// ES2026:一行搞定
const settings = new Map();
settings.set('language', 'en');

settings.getOrInsert('theme', 'dark');     // 'dark'(不存在,插入默认值)
settings.getOrInsert('language', 'pl');    // 'en'(已存在,返回原值)

// 经典缓存模式:
const cache = new Map();
const result = cache.getOrInsert(key, () => expensiveComputation());
// 只在 key 不存在时才执行昂贵计算

WeakMap 同样支持。别再手写那个 if-has-set-get 了。

八、Temporal API:Date 对象终于有救了

说起 Date,JS 开发者能吐槽半小时:

  • getMonth() 从 0 开始,一月是 0,十二月是 11(问就是 Java 遗毒)
  • new Date('2026-06-27') 在不同时区返回值不一样
  • 所有修改操作都是 mutable 的,很容易出副作用
  • "下个月最后一天"这种需求你得手动加一个月再判断是否跨月

Temporal 是 TC39 从零重新设计的日期时间库。先看一组对比:

javascript 复制代码
// ❌ Date:月份从 0 开始,不可变操作要手动处理
const d = new Date(2026, 5, 27);  // 5 = 六月,你猜猜看
d.setMonth(d.getMonth() + 1);     // 直接改了原对象!

// ✅ Temporal:语义清晰,不可变,月份从 1 开始
import { Temporal } from '@js-temporal/polyfill';

const today = Temporal.PlainDate.from({ year: 2026, month: 6, day: 27 });
const nextMonth = today.add({ months: 1 });  // 原 today 不变,返回新对象

console.log(today.toString());       // "2026-06-27"
console.log(nextMonth.toString());   // "2026-07-27"
console.log(today.dayOfWeek);        // 6(星期六)

几个实用的例子

javascript 复制代码
// 日期差计算
const start = Temporal.PlainDate.from('2026-01-01');
const end = Temporal.PlainDate.from('2026-07-15');
const diff = end.since(start, { largestUnit: 'month' });
console.log(`相差 ${diff.months} 个月又 ${diff.days} 天`);

// 时区转换,不用自己算
const shanghaiTime = Temporal.ZonedDateTime.from({
  timeZone: 'Asia/Shanghai',
  year: 2026, month: 7, day: 15, hour: 20,
});
const nyTime = shanghaiTime.withTimeZone('America/New_York');
console.log(nyTime.toString());
// "2026-07-15T08:00:00-04:00[America/New_York]"

// 计算下个月最后一天
const lastDay = Temporal.PlainDate.from('2026-07-31');
const nextLastDay = lastDay.add({ months: 1 });
// 自动处理月份天数差异

核心类型速查

类型 用途
PlainDate 纯日期,如 2026-07-15
PlainTime 纯时间,如 14:30:00
PlainDateTime 日期+时间,无时区
ZonedDateTime 带时区的完整时间
Instant 时间线上的绝对时刻(UTC)
Duration 时间段,如 2小时30分钟

提示 :Temporal 可以用 @js-temporal/polyfill 在所有环境使用,API 完全一致,体积不大。可以逐步替换项目中 dayjs/date-fns 的基础日期运算。

九、Pattern Matching:告别 if-else 金字塔

处理多种类型的分支判断时,JS 传统写法是真啰嗦:

javascript 复制代码
// 旧写法:if-else 套娃
function describe(value) {
  if (value === null || value === undefined) {
    return '空值';
  } else if (typeof value === 'number') {
    if (value > 0) return `正数:${value}`;
    if (value < 0) return `负数:${value}`;
    return '零';
  } else if (typeof value === 'string') {
    if (value.length > 10) return `长文本:${value.slice(0, 10)}...`;
    return `字符串:${value}`;
  } else if (Array.isArray(value)) {
    if (value.length === 0) return '空数组';
    return `数组共 ${value.length} 项`;
  }
  return '其他';
}

Pattern Matching 把这个场景变得清晰很多:

javascript 复制代码
// ES2026:match 表达式
function describe(value) {
  return value match {
    null     => '空值',
    n when n > 0  => `正数:${n}`,
    n when n < 0  => `负数:${n}`,
    0        => '零',
    s when typeof s === 'string' && s.length > 10
             => `长文本:${s.slice(0, 10)}...`,
    s when typeof s === 'string'
             => `字符串:${s}`,
    []       => '空数组',
    Array a  => `数组共 ${a.length} 项`,
    _        => '其他',
  };
}

嵌套解构也很自然

javascript 复制代码
// 处理各种可能的事件类型
const event = {
  type: 'payment_success',
  payload: {
    orderId: 'ORD-2026-0715',
    amount: 9900,        // 单位:分
    buyer: { id: 1024, vip: true },
  },
};

const notification = event match {
  { type: 'payment_success', payload: { orderId, amount, buyer: { vip: true } } }
    => `🎉 VIP 订单 ${orderId},实付 ¥${(amount / 100).toFixed(2)}`,
  { type: 'payment_success', payload: { orderId, amount } }
    => `✅ 普通订单 ${orderId},实付 ¥${(amount / 100).toFixed(2)}`,
  { type: 'refund', payload: { orderId, reason } }
    => `↩️ 退款:${orderId},原因:${reason}`,
  { type: 'error', payload: { code } } when code >= 500
    => `🚨 服务器内部错误:${code}`,
  { type: 'error', payload: { code } }
    => `⚠️ 请求异常:${code}`,
  _
    => '未知事件类型',
};
// "🎉 VIP 订单 ORD-2026-0715,实付 ¥99.00"

几个要点:

  • match表达式不是语句,可以赋给变量、作为 return 值
  • when 后面可以接任意布尔条件做守卫
  • _ 是万能兜底匹配
  • 支持深层嵌套解构

注意:Pattern Matching 目前是 Stage 3 提案,尚未进入正式规范,语法细节可能还有调整。

十、Records & Tuples:原生的不可变数据

如果你用过 React 或 Redux,一定对"不可变更新"不陌生。以前要么靠自觉(不用就行),要么找 Immer 这类库帮忙:

javascript 复制代码
// 旧方法:浅拷贝容易出 bug
const state = { user: { name: '小明' }, count: 1 };
const newState = { ...state };
newState.count = 2;

console.log(state.count);  // 还是 1 ✅
console.log(newState.count); // 2 ✅

// 但嵌套对象就翻车了
newState.user.name = '小红';
console.log(state.user.name);  // '小红' 😱 原对象被改了!

ES2026 引入的 Record(#{})和 Tuple(#[])是原生不可变的数据结构:

javascript 复制代码
// Record:不可变对象
const user = #{
  name: '小明',
  age: 28,
  scores: #[85, 92, 78],  // Tuple:不可变数组
};

// 所有"修改"操作都返回新对象
const older = user.with({ age: 29 });     // 原 user 不变
const withCity = user.with({ city: '上海' });

// 内容相等即引用相等
const user2 = #{ name: '小明', age: 28, scores: #[85, 92, 78] };
console.log(user === user2);  // true!✅

在 React 状态管理里怎么用

javascript 复制代码
// 用 Record 做状态,不用 Immer 了
let state = #{
  users: #[
    #{ id: 1, name: '小明', active: true },
    #{ id: 2, name: '小红', active: false },
  ],
  filter: 'all',
};

// 更新某个用户
state = state.with({
  users: state.users.map(u =>
    u.id === 1 ? u.with({ active: !u.active }) : u
  ),
});

// state 是新对象,React 的 shallow compare 能正确检测到变化
// 同时原始数据完全不受影响

深层内容相等的比较是 Records & Tuples 最大的优势------你不用再手写 isEqual,不用引入 lodash/isEqual,直接用 === 就行。这对 React 的 memouseMemouseEffect 依赖数组来说都是天大的好事。

注意:Records & Tuples 目前也是 Stage 3 提案,尚未进入正式规范。

总结

回头看这 10 个特性,会发现它们有一个共同点:没有一个是为了炫技,全是在填坑

  • using / await using 让资源管理从"手动挡"变成"自动挡",try-finally 地狱终于可以放进历史书了
  • Error.isError() 一行 API,干掉 instanceof Error 跨 realm 误判的十年老 bug
  • Math.sumPrecise() 用补偿求和算法保住累加精度,财务报表和购物车结算这些场景不用再提心吊胆
  • Array.fromAsync() 填补了异步迭代器转数组的空白,和 Array.from() 一样的体验,异步场景终于不用手动 push 了
  • Uint8Array 编解码 把 Base64 和 Hex 转换收进标准库,图片上传、哈希展示这些高频操作不再需要第三方依赖
  • Iterator.concat() 惰性拼接迭代器,大数据集不用全部展开到内存里再处理
  • Map.getOrInsert() 一行替代 if-has-set-get 四行样板,所有项目都有的缓存模式终于有了标准写法
  • Temporal API 从零重建日期时间处理,不可变、时区安全、语义清晰------Date 的黑历史到此为止
  • Pattern Matching 让多分支判断从杂乱无章的 if-else 堆叠变成结构清晰的 match 表达式
  • Records & Tuples 带来原生不可变数据,深层值相等直接 === 比较,不用再手写 isEqual

JavaScript 这十年走过的路很有意思:ES6 给了它现代化的骨架,而 ES2026 开始填血填肉。这些特性不会让你惊呼"黑科技",但会在某个下午突然发现------咦,这段代码以前不是要写十几行吗,现在怎么三行就搞定了。

相关推荐
小堂子这厢有礼了1 小时前
Chet.Admin 权限模型:菜单级 + 按钮级 + 行级三层防护
前端·后端·信息可视化·前端框架·.net
前端进阶之旅1 小时前
Next.js 16.3 新特性全解析 AI 驱动开发与 Instant Navigations 实战指南
开发语言·javascript·人工智能
Cobyte1 小时前
编译解析器的基础——状态模式与状态机的原理
前端·javascript·vue.js
IT_陈寒2 小时前
Vue的这个响应式陷阱,让我加班到凌晨三点
前端·人工智能·后端
2501_916007472 小时前
抓包工具对比 Charles、Fiddler、TraceEagle、Wireshark 与 Proxyman
前端·网络协议·ios·adb·https·fiddler·wireshark
小堂子这厢有礼了3 小时前
Chet.QuartzNet.UI v2.3.0 大更新!表格重构 + 系统配置 + 分析页全面升级!
前端·后端·ui·重构·c#·vue
触底反弹3 小时前
⚡ Promise.all 性能优化:让 Agent 的工具调用飞起来
javascript·人工智能·后端
程序员爱钓鱼3 小时前
Rust 控制流 if 详解:条件判断与 if 表达式
前端·后端·rust
心中有国也有家3 小时前
AtomGit Flutter 鸿蒙客户端:错误处理与优雅降级策略
android·javascript·flutter·华为·harmonyos