ES2021 到 ES2024,JavaScript 依然按部就班地"小步快跑"。这四年没有像 async/await 那样改变编程范式的大招,但积累起来相当可观:字符串的 replaceAll 让"替换全部"不再依赖正则;数组的 findLast 与一组不可变方法让数据处理更顺手;Promise.withResolvers 把"手动控制 Promise"的样板代码压缩成一行。本文按年份盘点这些特性,并给出对应的 Node.js 支持版本,方便你判断自己的环境能不能直接用。
1. ES2021:replaceAll 与逻辑赋值运算符
String.prototype.replaceAll 解决了 JavaScript 的一个老尴尬:replace 的第一个参数若是字符串,只替换第一处;想替换全部,要么写正则 /x/g,要么写循环。replaceAll 让"全部替换"成为默认行为,而且字符串参数不需要转义正则元字符,做模板变量替换、路径清洗这类工作非常顺手。对性能敏感的场景,它与全局正则的差别可以忽略,真正的价值在可读性:不用维护正则的 /g 状态,也不用担心元字符转义。需要留意:replaceAll 的正则参数必须带全局标志 g,否则会抛 TypeError,这一点与 replace 不同。同年还新增了三个逻辑赋值运算符 ||=、&&=、??=,把"先判断再赋值"压缩成一步;数字分隔符则让 1_000_000 比 1000000 好读得多。逻辑赋值运算符也有语义细节:??= 只在左侧为 null/undefined 时赋值,||= 只在左侧为假值时赋值,选择规则与 ??、|| 完全一致。
// replaceAll:替换所有匹配,不用再写正则
"2021-08-05".replaceAll("-", "/"); // "2021/08/05"
// 对比:replace 只替换第一处
"a-b-c".replace("-", "+"); // "a+b-c"
"a-b-c".replaceAll("-", "+"); // "a+b+c"
// 数字分隔符:大数字更易读
const budget = 1_000_000_000;
// 逻辑赋值运算符
let cache = null;
cache ??= compute(); // cache 为空才计算
let total = 0;
total ||= 10; // total 为假值才赋值
2. ES2022:私有字段、.at() 与顶层 await
class 的私有成员用 # 前缀标记,#balance 只能在类内部访问,从语言层面堵住了"约定 _ 前缀"这种靠自觉的方案。数组的 .at() 支持负数索引,arr.at(-1) 优雅地拿到最后一个元素,不用再写 arr[arr.length - 1]。顶层 await 则允许 ES 模块在顶层直接使用 await,加载配置文件、初始化连接等"模块加载期就要异步"的场景,不再需要包一层 async 函数。同版本还有 Object.hasOwn(替代 Object.prototype.hasOwnProperty.call 的长串写法)、Error 的 cause 选项,以及 static 静态初始化块,用来在类定义体里书写复杂的静态字段初始化逻辑。Object.hasOwn 与 in 运算符的差别在于:in 会沿着原型链查找,hasOwn 只看自身属性,判断"对象自己有没有这个键"时用 hasOwn 更准确。顶层 await 有个限制:只能在 ES 模块中使用,CommonJS 文件里依然会报语法错误,迁移时记得把 package.json 的 type 字段改成 module。
// 私有字段:# 开头,类外无法访问
class BankAccount {
#balance = 0;
deposit(amount) {
this.#balance += amount;
}
get balance() {
return this.#balance;
}
}
// acc.#balance; // SyntaxError,私有的
// .at():支持负数索引
const arr = ["a", "b", "c"];
arr.at(-1); // "c"
arr.at(0); // "a"
// 顶层 await(ES 模块中直接可用)
const config = await fetch("/config.json").then(r => r.json());
3. ES2023:findLast 与不可变数组方法
find 和 findIndex 只能从前往后找;以前要从后往前找,得先 reverse,既低效又容易出错。findLast/findLastIndex 直接补上这个方向,在"找最近一条记录""取最后一条匹配项"这类场景非常自然。更重要的是四个不可变方法:toSorted、toReversed、toSpliced 和 with,它们返回新数组而不修改原数组,与函数式风格和 React 这类"不可变数据"框架天然契合,也避免了 sort 原地修改带来的隐蔽 bug——排序后原数组悄悄变掉,是很多诡异问题的来源。toSorted 默认按字符串顺序排序,与 sort 一样可以传入比较函数;with 适合"替换某一位"的不可变更新,和 React 里 setState 的更新模式很搭。
// findLast:从后往前找
const students = [
{ name: "A", score: 60 },
{ name: "B", score: 90 },
{ name: "C", score: 80 }
];
const lastPass = students.findLast(s => s.score >= 60);
// { name: "C", score: 80 }
// 不可变方法:返回新数组,原数组不变
const nums = [3, 1, 2];
const sorted = nums.toSorted(); // [1, 2, 3]
const reversed = nums.toReversed(); // [2, 1, 3]
const updated = nums.with(0, 99); // [99, 1, 2]
console.log(nums); // [3, 1, 2],没变
// toSpliced:删除/替换元素也不动原数组
const list = ["a", "b", "c", "d"];
list.toSpliced(1, 2); // ["a", "d"],原数组不变
4. ES2024:Promise.withResolvers 与 Object.groupBy
Promise.withResolvers 一次性返回 { promise, resolve, reject } 三件套,专门服务"由外部事件决定何时完成"的 Promise:事件监听器、超时控制、游戏循环等场景,再也不用先声明两个变量、再包一层 new Promise。Object.groupBy 与 Map.groupBy 让"按字段分组"成为标准库能力,处理接口返回的列表数据非常常用。同版本还有 Array.fromAsync(把异步可迭代对象收集为数组)和 String.isWellFormed / toWellFormed(处理代理对不完整的畸形字符串)等。Promise.withResolvers 还有一个典型场景:把回调风格的旧 API 包装成 Promise,配合超时兜底,比手写 new Promise 清晰很多。Map.groupBy 与 Object.groupBy 的区别在于返回 Map,键可以是任意类型,分组键是对象或需要保留插入顺序时优先考虑它。
// 之前:手动控制 Promise 需要先声明两个变量
let resolveFn, rejectFn;
const p = new Promise((resolve, reject) => {
resolveFn = resolve;
rejectFn = reject;
});
// 之后:一行拿到三个工具
const { promise, resolve, reject } = Promise.withResolvers();
// 适合事件驱动场景
socket.on("data", resolve);
socket.on("close", () => reject(new Error("closed")));
await promise;
// Object.groupBy:按规则分组
const inventory = [
{ name: "苹果", type: "水果" },
{ name: "白菜", type: "蔬菜" }
];
const byType = Object.groupBy(inventory, item => item.type);
// { 水果: [苹果], 蔬菜: [白菜] }
// Array.fromAsync:异步遍历转数组
const files = await Array.fromAsync(readFiles());
5. 如何在你的环境里启用
这四年的特性支持基本可以按年份对齐:2021 年的特性需要 Node 15+,2022 年的需要 Node 16.6+(顶层 await 需要 14.8+ 的 ESM 环境),2023 年的需要 Node 20+,2024 年的需要 Node 21 或 22+。也就是说,Node 22 LTS(2024 年进入 LTS)可以覆盖本文全部特性;浏览器方面,2023 年后的 Chrome、Firefox、Safari 也基本全部支持。如果团队还停留在 Node 18,只有 2023/2024 年的部分特性不可用,可以考虑按需 polyfill 或规划升级。动手前也可以用一行 node -e 脚本检查 Array.prototype.findLast 之类的 API 是否存在,确认环境支持度。判断某个方法是否存在,用 typeof 检查最稳妥,例如 typeof arr.at === 'function'。浏览器兼容性可以对照 caniuse.com 的对应条目,对 2023 年后的设备,这些特性基本都已就绪。
| 特性 | 版本 | Node.js 支持 | 主要浏览器 |
|---|---|---|---|
| replaceAll | ES2021 | 15+ | Chrome 85+ |
| 逻辑赋值运算符 | ES2021 | 15+ | Chrome 85+ |
| class 私有字段 | ES2022 | 12+ | Chrome 74+ |
| .at() | ES2022 | 16.6+ | Chrome 92+ |
| 顶层 await | ES2022 | 14.8+(ESM) | Chrome 89+ |
| findLast / toSorted | ES2023 | 20+ | Chrome 110+ |
| Promise.withResolvers | ES2024 | 22+ | Chrome 119+ |
| Object.groupBy | ES2024 | 21+ | Chrome 117+ |
6. 值不值得用
这四年的特性大多属于"低风险高收益"的语法糖:replaceAll 和 .at() 日常必用,findLast 遇到就赚到,不可变数组方法值得在 React 项目里全面采用,Promise.withResolvers 能显著简化事件驱动代码。真正的取舍在于项目底线:如果必须支持 Node 18 或旧浏览器,优先使用 2021/2022 年的特性,2023/2024 年的按需 polyfill 或等升级。另外注意 Object.groupBy 返回的对象没有原型,直接用键访问没问题,但别依赖它上面的 Object 原型方法,需要时先转成普通对象或改用 Map.groupBy。总的来说,这四年的特性虽然单个体量小,却恰好覆盖了日常编码里最常遇到的痛点,属于"用上就回不去"的那一类。
💡 学习建议:把开发环境升级到 Node 22 LTS,然后打开项目里最常用的一个工具函数文件,用 replaceAll、.at()、findLast 和不可变方法各找一处可以替换的写法,感受新版语法带来的整洁度提升。