看 ES2026 和 ESNext 新特性时,我更关心它什么时候能进业务代码
JavaScript 每年都会继续演进。
到 2026 年再看新标准,最重要的不是把所有新 API 都塞进“ES2026”这个篮子里,而是先分清它们到底处在哪个阶段:哪些已经进了 ES2025,哪些属于 ES2026,哪些只是 ESNext 或后续年度值得关注的方向。
但我现在看新标准,不会只问“这个语法酷不酷”,而是先问:它什么时候能进业务代码,进了以后是不是真的让项目更好维护。
这几年踩过的坑让我对“新特性”这件事变得越来越冷静。早些年我特别爱在项目里第一时间用上刚定稿的语法,结果有一次用了可选链 ?. 配合老版本 UglifyJS,构建直接报 parse error,半夜回滚了一版本才发现是压缩工具没跟上。从那以后我看 TC39 的提案,第一反应不是“真香”,而是“这玩意儿在我这条工具链上能不能活下来”。
这篇里我会把时间线拆开说:Iterator Helpers、Set 集合运算、Promise.try、RegExp.escape 更像是 ES2025 已经落地的成果;Array.fromAsync、Error.isError 属于 ES2026 这一轮可以重点看的补丁;Explicit Resource Management(using / await using)和 Temporal 则更适合先放在 ESNext / 后续年度的观察区,不要写成 ES2026 已经稳定可用。
新特性不要急着全用
看到新标准时,最容易犯的错是马上在业务代码里到处使用。
但实际项目要先考虑:
- 浏览器是否支持
- 构建工具是否能转译
- 团队是否理解
- 代码可读性是否真的变好
新语法是工具,不是炫技材料。
我一般会把新特性分成三类:
1可以关注:还在提案或生态讨论阶段,先理解方向 2可以试用:构建工具能稳定处理,适合内部工具或低风险页面 3可以推广:主流运行环境支持,团队也理解它的语义和边界
很多新能力刚出现时,只适合第一类。不要因为技术文章写得热闹,就直接放进核心业务。
我有个简单的判断习惯:先去 caniuse 和 MDN 的兼容性表查一眼,再去看自己用的 SWC / esbuild / Babel 的 release notes 里有没有这条 transform。如果一个特性需要运行时原生支持(比如 Temporal、using 的 Symbol.dispose),但目标环境覆盖不到,那它对我来说就只能停在“可以关注”那一档。
举个真实例子。Iterator Helpers 我很想用,因为它能把链式数据处理写得很顺:
1// 想要:从一个超大日志迭代器里取前 100 条 error,惰性求值,不构建中间数组 2const firstErrors = logs 3 .values() 4 .filter(line => line.level === 'error') 5 .map(line => line.message) 6 .take(100) 7 .toArray();
想自己验证这套链式方法是否可用,可以跑一个最小例子(Iterator Helpers 已进入 Stage 4、ES2025,Node 22+ / Chrome 122+ 原生支持;老环境需 core-js polyfill):
1[1, 2, 3, 4].values().map((x) => x * 2).take(2).toArray(); 2// [2, 4]
注意 .values() 先把数组变成迭代器,后面的 .map / .take 才是挂在 Iterator.prototype 上的惰性方法——直接对数组调 .take 是没有的。
这写法的好处是惰性——take(100) 满了就不再往下消费,省掉了 Array.from(logs).filter(...).slice(...) 那种先把整个数组物化再切片的浪费。但我在一个还要兼容 iOS 15 Safari 的项目里就没敢直接上,因为这些原型方法是挂在 Iterator.prototype 上的运行时能力,Babel 转译不了语法只能靠 core-js polyfill,而 polyfill 整个 Iterator helper 体积不小。最后我退回到 core-js 按需引入 + 只在内部后台用,公共 SDK 里依然手写循环。这就是“分三档”落到实处的样子。
不可变数据越来越重要
现代前端里,不可变数据已经很常见。
React 状态更新、时间旅行调试、缓存比较、数据快照,都更喜欢不可变思路。
如果标准层面继续补充不可变相关能力,前端代码就能减少一些手写复制和第三方工具依赖。
1const next = [...list, newItem];
这类写法背后的目标是:不要直接改旧数据,而是生成新数据。
我更关心的是它能不能减少“看不见的原地修改”。
在复杂中后台里,很多 bug 都来自状态被悄悄改了:
1const next = config; 2next.rules[0].enabled = false; 3setConfig(next);
这段代码表面上更新了配置,实际引用没变,React 可能不更新,缓存比较也可能失效。不可变数据相关的新能力如果能让复制和更新更清楚,就有实际价值。
这里我特别想提一下 ES2023 就进来、但到现在很多人还没用顺的几个“非破坏性数组方法”:toSorted、toReversed、toSpliced、with。它们的意义就是把原本会原地改数组的操作,变成返回新数组:
1// 旧写法:sort 会原地改 list,直接踩 React 的引用比较 2const sorted = list.sort((a, b) => a.order - b.order); // sorted === list,坑 3 4// 新写法:toSorted 返回新数组,原数组不动 5const sorted = list.toSorted((a, b) => a.order - b.order); 6 7// 改单个元素也不用展开拼接了 8const next = list.with(2, { ...list[2], enabled: false });
我在 code review 里见过太多次 arr.sort() 之后还以为原数组没变的 bug,尤其是 sort 默认按字符串排序把数字 [1, 2, 10] 排成 [1, 10, 2] 那种二连坑。toSorted 把“不可变”这件事变成默认姿势,是我现在会主动推广的特性——因为它确实降低出错概率,而不是单纯让代码短。
至于深拷贝,现在我基本用原生 structuredClone,能处理循环引用、Map、Set、Date、TypedArray,比 JSON.parse(JSON.stringify(x)) 靠谱太多(后者会丢 undefined、把 Date 变字符串、遇到 function 直接报错或丢字段):
1const snapshot = structuredClone(config); // 深拷贝快照,留作时间旅行调试
不过,不可变也不是无脑深拷贝。配置很大时,复制成本也要考虑——structuredClone 一个几 MB 的大对象在低端机上是会卡帧的。真正重要的是清楚表达“哪里变了”,做局部的浅复制,而不是每次都把整棵对象复制一遍。这也是为什么 Immer 那种“结构共享 + 写时复制”的思路一直有市场:它只复制改动路径上的节点,其余引用照旧共享。
资源管理让清理更明确
前端代码里也有很多需要清理的资源:
- 定时器
- 事件监听
- WebSocket
- 文件句柄
- 流式连接
如果语言层面提供更清晰的资源管理能力,就能减少“创建了但忘记清理”的问题。
这对复杂页面、长连接和富交互应用很有意义。
我在项目里最常见的资源泄漏,通常不是很底层的文件句柄,而是这些:
- 页面卸载后定时器还在跑
- 弹窗关闭后事件监听没移除
- 切换会话后旧 SSE 连接还在推数据
- WebSocket 重连后旧连接没断干净
- 大文件预览的 object URL 没释放
所以看资源管理新能力时,我会先想它能不能让“创建和清理”离得更近。
即使暂时不用新语法,也可以先养成这种结构:
1function createChatStream() { 2 const controller = new AbortController(); 3 4 return { 5 signal: controller.signal, 6 dispose() { 7 controller.abort(); 8 } 9 }; 10}
核心思想是一样的:资源不是创建完就结束了,清理路径也要成为设计的一部分。
Explicit Resource Management 的 using 声明就是把这套思路语法化了。这里要把年份说准:它值得关注,但不要把它当作 ES2026 已经稳定落地的业务默认能力。它依赖对象实现 Symbol.dispose(同步)或 Symbol.asyncDispose(异步),变量离开作用域时自动调用清理,行为有点像 C# 的 using 或 Python 的 with:
1function openChannel(url) { 2 const ws = new WebSocket(url); 3 return { 4 ws, 5 [Symbol.dispose]() { 6 ws.close(); 7 console.log('channel disposed'); 8 } 9 }; 10} 11 12{ 13 using channel = openChannel('wss://example.com'); 14 channel.ws.send('hello'); 15 // 块结束时自动调用 Symbol.dispose,不用记着手动 close 16}
异步版本配合 await using,对我那种“切换会话后旧 SSE 连接还在推数据”的场景特别对症:
1async function streamChat(query) { 2 await using stream = createChatStream(query); 3 for await (const chunk of stream.read()) { 4 render(chunk); 5 } 6 // 函数退出(正常或抛错)都会 await stream[Symbol.asyncDispose]() 7} 8 9function createChatStream(query) { 10 const controller = new AbortController(); 11 // ...发起 fetch + ReadableStream 12 return { 13 read() { /* async generator */ }, 14 async [Symbol.asyncDispose]() { 15 controller.abort(); 16 } 17 }; 18}
最打动我的一点是它的“异常安全”:哪怕中间 throw 了,dispose 也会被调用,等价于自动帮你写了 try/finally。过去我清理逻辑漏掉的,十有八九都是 early return 或抛异常那条没覆盖到的分支。多个 using 还会按声明的逆序释放,跟手写嵌套 finally 的顺序一致,这点设计得很克制。
配套还有一个 DisposableStack / AsyncDisposableStack,可以把一批要清理的东西集中登记,适合“一个组件挂载时建了好几样资源”的情况:
1function setupWidget(el) { 2 const stack = new DisposableStack(); 3 const timer = setInterval(tick, 1000); 4 stack.defer(() => clearInterval(timer)); 5 6 const onResize = () => layout(el); 7 window.addEventListener('resize', onResize); 8 stack.defer(() => window.removeEventListener('resize', onResize)); 9 10 return stack; // 调用方拿到 stack,dispose 时一次性全清掉 11}
当然,这东西要真正用上,得运行时支持 Symbol.dispose / Symbol.asyncDispose,或者依赖 TypeScript 5.2+ 的 downlevel 编译。我目前的态度还是:内部工具、Node 脚本里可以小范围试,面向老浏览器的线上代码先观望,但写法结构已经可以提前对齐——把 dispose 的责任显式化,比语法本身更重要。
那些不起眼但天天能用的小补丁
比起大特性,我反而更喜欢这一波里几个“补缺口”的小东西,因为它们落地成本低、踩坑收益高。
Set 的集合运算终于进标准了。以前求两个集合的交集差集,要么转数组 filter,要么手写循环,现在直接:
1const a = new Set([1, 2, 3, 4]); 2const b = new Set([3, 4, 5]); 3 4a.intersection(b); // Set { 3, 4 } 5a.union(b); // Set { 1, 2, 3, 4, 5 } 6a.difference(b); // Set { 1, 2 } 7a.isSubsetOf(b); // false
这几个方法返回的都是新 Set,原集合不动。它们属于 ES2025 已经落地的能力,不应该再算作 ES2026 新特性;老环境仍需 core-js polyfill。
做权限标签、做“选中项 vs 全量”的 diff,这几个方法能省掉一大坨可读性很差的 filter 嵌套。
Promise.try 也是 ES2025 这一批里很实用的小补丁,用来统一同步/异步函数的错误通道。过去一个回调可能同步抛错、也可能返回 rejected promise,调用方得写两套处理;现在一律包进 promise 链:
1// fn 可能同步 throw,也可能返回 Promise 2Promise.try(fn) 3 .then(handleResult) 4 .catch(handleError); // 同步抛的错也会走到这里
Error.isError 更接近 ES2026 这轮要看的补洞能力,它解决一个老问题:instanceof Error 跨 iframe / 跨 realm(比如 Node 的 vm、worker 之间传过来的对象)会失效,因为 Error 构造函数不是同一个。Error.isError(x) 是按内部标记判断的,更可靠:
1try { 2 doWork(); 3} catch (e) { 4 if (Error.isError(e)) console.error(e.stack); 5 else console.error('非 Error 抛出物:', e); // 有人 throw 字符串 6}
RegExp.escape 也已经是 ES2025 的成果,它终于让“把用户输入安全地拼进正则”这件事有了官方答案,再也不用从 Stack Overflow 抄那段 replace(/[.*+?^${}()|[\]\\]/g, '\\$&') 了:
1const keyword = userInput; // 可能含有 . * ( ) 等正则元字符 2const re = new RegExp(RegExp.escape(keyword), 'gi'); 3text.replace(re, '<mark>$&</mark>');
还有 Array.fromAsync,这是 ES2026 更值得单独看的一个 API:对着异步可迭代对象一把收集成数组,配合分页接口或流式数据很顺手:
1const all = await Array.fromAsync(paginate('/api/items'));
这些东西没什么“技术含量”可炫,但恰恰是它们让日常代码少了一堆容易出错的样板。我推广新特性时,往往是从这类小补丁开始的——团队接受度高,因为它解决的是大家都被烦过的具体问题。
兼容和转译要提前查清楚
JavaScript 新特性落地时,最容易低估的是工具链。
有些语法可以通过 Babel 或 SWC 转译,有些能力依赖运行时原生支持,有些需要 polyfill,有些根本不适合 polyfill。它们的落地成本不一样。
我会先问:
- 目标浏览器和 Node 版本是否支持
- 构建工具能不能稳定处理
- 是否需要 polyfill
- polyfill 体积是否值得
- 调试后的代码是否还容易理解
- 团队代码规范是否允许使用
如果这些问题答不清楚,我会先把新特性限制在内部工具或低风险模块,而不是直接写进公共组件库。
这里要分清两件事:语法特性和运行时 API。语法特性(比如 using 声明、装饰器)能靠 Babel/SWC/TS 把它降级成老语法,编译期就解决了;运行时 API(比如 Set.prototype.intersection、Array.fromAsync、Iterator.prototype.map)只能靠 polyfill 在运行时补,转译器帮不上忙。我吃过的亏就是把这两类混为一谈——以为 @babel/preset-env 一配就万事大吉,结果 Array.fromAsync 在线上老机器上 undefined is not a function 直接白屏。
我现在配 Babel 一定把 useBuiltIns: 'usage' 加上 corejs 版本写准,让它按实际用到的 API 自动注入 polyfill:
1// babel.config.js 2module.exports = { 3 presets: [ 4 ['@babel/preset-env', { 5 useBuiltIns: 'usage', 6 corejs: { version: '3.37', proposals: true }, // 只在确实要试 proposal 时打开 7 targets: { browsers: ['> 0.5%', 'last 2 versions', 'not dead'] } 8 }] 9 ] 10};
proposals: true 这行不要无脑开。已经进标准的 API,优先按目标环境和 core-js 版本确认;还在 proposal 通道里的能力,才需要把这道门打开。进生产前还要用真机或者 BrowserStack 验一遍,devtools 里跑得好不代表用户的旧设备跑得好。
学新特性时看使用场景
判断一个新特性是否值得用,可以问三个问题:
- 它能不能减少重复代码
- 它能不能降低出错概率
- 它能不能让意图更清楚
如果答案都是否定的,就没必要为了新而新。
拿 Temporal 来说,它是我最期待、但也最克制不直接全量推的一个。它不该被写成 ES2026 已稳定落地的能力,但非常适合拿来说明“什么样的新 API 值得持续跟”。前端处理日期一直是重灾区:Date 的月份从 0 开始、时区永远是本地时区、new Date('2026-04-24 10:00:00') 这类不带 T 分隔符的日期时间字符串在不同浏览器里解析结果不一致(Chrome/Node 按本地时间,旧版 Safari 按 UTC 甚至直接 Invalid Date)。Temporal 把这些都重做了一遍,时区、日历、时间段都是显式的:
1// 不会再有“月份从 0 开始”这种反人类设计 2const date = Temporal.PlainDate.from('2026-04-24'); 3date.add({ days: 7 }).toString(); // '2026-05-01' 4 5// 时区是显式的,不再默默用本地时区 6const meeting = Temporal.ZonedDateTime.from( 7 '2026-04-24T10:00:00[Asia/Shanghai]' 8); 9meeting.withTimeZone('America/New_York').toString();
但 Temporal 是个体积不小的运行时 API,polyfill(@js-temporal/polyfill)压缩后也有几十 KB。所以我现在的做法是:新项目里凡是涉及跨时区、排班、计费周期这种“日期逻辑复杂”的模块才引入它;只是格式化展示一个时间戳,老老实实用 Intl.DateTimeFormat 就够了,没必要为了用新 API 把包体撑大。判断标准还是那三条——能不能减少出错、能不能让意图更清楚、值不值这个成本。
我自己的一条经验:评估一个特性时,最好找一段现有的、被它困扰过的真实代码来做对照。把旧写法和新写法并排放一起,diff 出来如果是“更短 + 更不容易错 + 团队一眼能懂”,那就值得推;如果只是“更短但要查文档才懂”,那就先放进内部项目练手,别急着写进所有人都要维护的核心库。
还有一个问题也很重要:它会不会让团队里大多数人读起来更轻松。
新语法有时候能减少代码,但如果团队需要停下来查语义,维护成本未必下降。真正值得推广的特性,应该是用几次之后大家都觉得“这样表达更自然”,而不是只有写的人觉得高级。
ES2026 值得关注,ESNext 也值得持续看,但都不需要盲目追。
这一轮标准演进让我留意的,是三条平时容易被当成"团队纪律"来维护的东西,正在一点点变成语言自己就管的事:using 这类提案把资源清理从"记得要 close"变成块结束自动触发;toSorted / with 这类方法让不可变成了调用一个方法就有的默认结果,不用再靠 code review 盯着谁又原地改了数组;Set 运算、Array.fromAsync 这批小补丁把以前团队里手写、各自实现细节还不一样的样板,收进了标准库统一的写法。
落地节奏上,我现在的做法是先在内部脚本和后台工具里试、攒够踩坑经验,再写进团队的 ESLint 规则或脚手架模板让新写法变成默认,最后才轮到面向外部用户的核心代码——每一档都留足验证时间。ES2025 那批已经稳定的方法我现在会主动推广,ES2026 和 ESNext 这批还在观察区的,我情愿多等一个版本的兼容表更新,也不想让一个没查清 polyfill 体积的新特性在线上半夜给我打电话。