前言
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 的作用就一句话:声明在哪个大括号里,离开那个大括号的时候就自动释放 。不管是 return、throw 还是正常执行完,它都会帮你收尾。
原理长什么样
任意对象只要实现 [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,内部都是一个独立的小宇宙,各自有一套完整的 Error、TypeError、RangeError 构造函数。
想象一下: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 的 memo、useMemo、useEffect 依赖数组来说都是天大的好事。
注意:Records & Tuples 目前也是 Stage 3 提案,尚未进入正式规范。
总结
回头看这 10 个特性,会发现它们有一个共同点:没有一个是为了炫技,全是在填坑。
using/await using让资源管理从"手动挡"变成"自动挡",try-finally 地狱终于可以放进历史书了Error.isError()一行 API,干掉instanceof Error跨 realm 误判的十年老 bugMath.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 开始填血填肉。这些特性不会让你惊呼"黑科技",但会在某个下午突然发现------咦,这段代码以前不是要写十几行吗,现在怎么三行就搞定了。