今年 ECMAScript 2024 标准已经定稿,浏览器和 Node.js 22 都跟得很快。不需要 Babel 插件,不需要等几个月,现在打开控制台就能用上其中大部分特性。本文挑了五个最常用、最能减少胶水代码的新方法,每个都配了实际业务里会遇到的场景,读完你就能立刻在项目里用起来。
一、Promise.withResolvers() —— 手动构造 Promise 彻底成为历史
有太多的场景需要把一个回调或事件转成 Promise,比如等待用户点击、封装文件读取、或者把 EventEmitter 的事件转换成 await 形式。以前的做法是绕不开 new Promise((resolve, reject) => { ... }),然后把 resolve 和 reject 存到外部变量里,代码看起来总是歪歪扭扭的。
Promise.withResolvers() 直接返回一个对象,里面包含 promise、resolve 和 reject,而且这些都是已经绑定了正确的 this 的函数。不需要在构造函数里套一层,也不需要担心闭包泄漏。
真实案例:把用户的一次点击转为 Promise
// 旧写法:必须用外层变量保存 resolve
function waitForClick(button) {
let resolveClick;
const promise = new Promise((resolve) => {
resolveClick = resolve;
});
button.addEventListener('click', resolveClick, { once: true });
return promise;
}
// 新写法:干干净净
function waitForClick(button) {
const { promise, resolve } = Promise.withResolvers();
button.addEventListener('click', resolve, { once: true });
return promise;
}
// 使用方式完全一致
const confirmButton = document.querySelector('#confirm');
waitForClick(confirmButton).then(() => {
console.log('用户点击了确认按钮');
});
这个场景在实际项目里出镜率极高:对话框的确认、倒计时结束、动画完成。以前要么用外部变量,要么把事件绑定塞进 Promise 构造函数,现在两行就搞定了。更常见的是把它封装成一个通用工具:
function eventToPromise(target, eventName) {
const { promise, resolve } = Promise.withResolvers();
target.addEventListener(eventName, resolve, { once: true });
return promise;
}
// 等一个动画结束
await eventToPromise(animationElement, 'animationend');
简洁到这个程度,以前专门引入的 p-event 这类第三方库真的可以考虑退役了。
二、Object.groupBy() —— 别再用 reduce 手动分组了
把数组按某个属性分组是高频操作。lodash 的 _.groupBy 用得很顺手,原生 JS 里却只能自己拿 reduce 堆,代码一长看起来就累。ES2024 的 Object.groupBy() 把这件事变成了标准方法,返回一个普通对象,键是分组的依据,值是分组后的数组。还有一个 Map.groupBy(),返回 Map 实例,键可以是任意类型。
按年龄分组用户列表
const users = [
{ name: '张伟', age: 28, role: '开发' },
{ name: '王芳', age: 32, role: '设计' },
{ name: '李娜', age: 28, role: '测试' },
{ name: '赵强', age: 35, role: '开发' },
];
// 按年龄分组
const byAge = Object.groupBy(users, user => user.age);
console.log(byAge);
// {
// '28': [{name:'张伟',...}, {name:'李娜',...}],
// '32': [{name:'王芳',...}],
// '35': [{name:'赵强',...}]
// }
// Map.groupBy 可以用对象作为键
const roles = [
{ name: '首页', module: 'dashboard' },
{ name: '用户管理', module: 'admin' },
{ name: '系统设置', module: 'admin' },
];
const byModule = Map.groupBy(roles, item => item.module);
// Map(2) { 'dashboard' => [...], 'admin' => [...] }
对比一下以前用 reduce 的实现:
// 旧写法,需要手动初始化对象
const byAgeOld = users.reduce((acc, user) => {
const key = user.age;
if (!acc[key]) {
acc[key] = [];
}
acc[key].push(user);
return acc;
}, {});
Object.groupBy 不仅代码量少,而且意图清晰——一眼就知道在分组。配合 Map.groupBy 还能解决键是非字符串的情况,比如用日期对象、数字甚至 Symbol 作为分组依据,这在以前可是要绕不少弯路的。
三、Array.fromAsync() —— 异步数据源转数组的标准姿势
异步迭代器在 Node.js 流、fetch 的分块读取、分页 API 中越来越常见。把异步迭代器转成数组以前没有直接的方法,通常需要自己写 for await...of 循环或者用第三方工具。Array.fromAsync() 就是专门做这件事的:接收一个异步可迭代对象或类数组,返回一个 Promise,resolve 出来的是完整数组。
场景:分页 API 全量拉取
// 模拟一个分页返回的异步生成器
async function* fetchPages() {
let page = 1;
let hasMore = true;
while (hasMore) {
const response = await fetch(`/api/data?page=${page}`);
const { items, totalPages } = await response.json();
yield* items; // 逐个产出数据项
hasMore = page < totalPages;
page++;
}
}
// 不用 for await...of,一行拿到全部数据
const allItems = await Array.fromAsync(fetchPages());
console.log(`总共获取到 ${allItems.length} 条记录`);
如果没有 Array.fromAsync,你需要这样写:
const allItems = [];
for await (const item of fetchPages()) {
allItems.push(item);
}
逻辑没问题,但就是多出了几行”管道”代码。而且 Array.fromAsync() 还支持第二个参数作为映射函数,可以像 Array.from 一样对每一项做转换:
// 直接映射成只取 id
const ids = await Array.fromAsync(fetchPages(), item => item.id);
对于处理流式数据、逐条读取文件行、或者订阅 WebSocket 消息并聚合成数组的场景,这个方法能让代码的意图表达得更直接。
四、正则表达式 v 标志 —— 更强大的 Unicode 模式
正则的 u 标志大家很熟,但它在处理复杂 Unicode 时有时还是不够精确。v 标志可以理解为 u 的升级版,额外提供了两个重要能力:集合运算(差集、交集)和扩展的属性转义。
集合运算:匹配字母但排除数字
// 旧的 u 标志下如果要做排除是很麻烦的
// 现在 v 标志下直接用 -- 表示差集
const regex = /[p{Letter}--p{ASCII_Hex_Digit}]/v;
console.log(regex.test('a')); // false, 因为 a 是十六进制数字,被排除了
console.log(regex.test('z')); // true, z 不是十六进制数字
console.log(regex.test('哈')); // true, 中文字母,不在十六进制范围内
这在处理复杂的字符范围需求时极其有用。比如你要匹配中文汉字但排除标点符号,或者匹配数字但排除特定数字,一行正则就能表达清楚,完全不需要在正则外用 filter 做二次处理。
属性转义增强:更细粒度的 Unicode 属性
v 标志还支持更多 Unicode 属性,比如 p{RGI_Emoji} 只匹配标准推荐的 emoji 序列,而不是零散的单字节。做国际化文本处理时这些细节直接决定了准确性。
// 匹配一个完整的 emoji(包括肤色修饰符)
const emojiRegex = /p{RGI_Emoji}/v;
console.log(emojiRegex.test('👋🏼')); // true,作为一个整体匹配
v 标志目前 Chrome 和 Node.js 22 已经支持,Firefox 和 Safari 也在跟进。现在写正则,涉及到 Unicode 的,可以优先考虑用 v 标志来替代 u 标志,毕竟它是 u 的严格超集。
五、ArrayBuffer 的 resize 和 transfer —— 二进制操作更灵活
处理音视频、WebSocket 二进制帧、或者大文件切片时,ArrayBuffer 的长度一直是固定的,想调整大小就得复制一份新的,造成不必要的内存开销和性能损耗。ES2024 新增了 resize() 和 transfer() 方法,前者可以在原地调整大小(需要预先创建可调整大小的 buffer),后者可以将 buffer 的所有权转让给一个新的 buffer,旧 buffer 被分离无法再用。
动态拼接二进制数据
// 创建一个最大可扩容到 1024 字节的 buffer,初始 0 字节
const buffer = new ArrayBuffer(0, { maxByteLength: 1024 });
const view = new Uint8Array(buffer);
// 模拟收到数据块,动态扩容
function appendChunk(chunk) {
const oldLength = view.byteLength;
buffer.resize(oldLength + chunk.byteLength);
// 注意:resize 后之前的 view 仍然有效,因为底层内存可能重分配了,但规格保证视图自动适配
view.set(chunk, oldLength);
}
appendChunk(new Uint8Array([72, 101, 108])); // "Hel"
appendChunk(new Uint8Array([108, 111])); // "lo"
console.log(new TextDecoder().decode(view)); // "Hello"
这个写法和直接用数组 push 很像,但操作的是原始的二进制内存,避免了反复分配大数组带来的 GC 抖动。在做 WAV 文件拼接、实时视频帧合成时特别实用。
transfer() 则适合把 buffer 交给 Web Worker 处理,主线程不再持有引用,避免了拷贝大内存:
const largeBuffer = new ArrayBuffer(10 * 1024 * 1024); // 10MB
// 转移所有权给 Worker,不复制
worker.postMessage({ buffer: largeBuffer }, [largeBuffer]);
console.log(largeBuffer.byteLength); // 0,原 buffer 已被分离
这些方法让 JavaScript 处理二进制数据的能力向 C 这类系统语言又靠近了一步。
六、浏览器和 Node.js 支持情况一览
这些特性不是提案,是已经写入标准的。具体支持情况:
- Promise.withResolvers():Chrome 119+,Node.js 22+,Firefox 121+,Safari 17.4+
- Object.groupBy / Map.groupBy:Chrome 117+,Node.js 21+,Firefox 119+,Safari 17.4+
- Array.fromAsync():Chrome 121+,Node.js 22+,Firefox 120+,Safari 17.4+
- 正则 v 标志:Chrome 112+,Node.js 20+,Firefox 116+,Safari 17+
- ArrayBuffer resize/transfer:Chrome 111+,Node.js 20+,Firefox 118+,Safari 17.4+
Node.js 22 已经全面覆盖了这五项特性,前端只要不兼容 IE 或者很老的 Safari 版本,基本都可以直接用。如果还有需要兼容旧浏览器的项目,Babel 和 core-js 的对应 polyfill 也已经在路上了,但实话实说,这些方法大多是对现有逻辑的语法简化,不是不可或缺的新能力,渐进式迁移完全可行。
七、总结
ES2024 这次新增的特性没有翻天覆地的改变,但几乎每一个都精准地解决了日常开发中的痒点。Promise.withResolvers 消灭了手动 Promise 的外层变量,Object.groupBy 让分组操作回归语义化,Array.fromAsync 补齐了异步迭代器的最后一块拼图,正则 v 标志让 Unicode 处理不再蹩脚,ArrayBuffer 的新增方法则让二进制数据处理更加高效。
这些方法的共同特点是:它们替代的都是之前不得不写一堆样板代码或者引入第三方库才能完成的事情。现在原生就支持了,代码量减少的同时,可读性反而提升不少。打开控制台试一试,也许下一个提交你就能删掉项目里那个自己封装的 groupBy 工具函数。

