现代JavaScript的16个实用新特性:从ES2022到ES2025
一、ES2022:填补日常痛点
1. Error.cause
写程序的人,谁没遇到过这种场景:一个错误引发了另一个错误,另一个错误又被新的包装吞掉,最后你排查半天,真正最初的原因早就找不到了。
以前遇到这种情况,我们通常只能二选一:要么直接覆盖原始错误,要么手动往错误对象上挂一堆自定义属性。能用,但不优雅。
现在有了Error.cause,这件事终于正规化了:
throw new Error("加载用户数据失败", {
cause: realError
});示例:
try {
await fetchUser();
} catch (err) {
throw new Error("用户加载失败", { cause: err });
}这样做最大的好处是错误上下文不会断。出问题时,你能顺着整条链往回追,而不是看着一层层包装过的报错发呆。
2. Object.hasOwn()
以前我们想判断某个属性到底是不是对象“自己”的,而不是从原型链上继承来的,通常会这么写:
Object.prototype.hasOwnProperty.call(obj, "key");现在简单多了:
Object.hasOwn(obj, "key");示例:
const user = { name: "John" };
Object.hasOwn(user, "name"); // true
Object.hasOwn(user, "toString"); // false这种变化看似很小,但恰恰就是这种“小而准”的API,最能改善日常写代码时的负担。
3. 顶层await
以前在模块最外层你是不能直接写await的。这就导致一个尴尬的情况:明明只是做一点启动初始化,比如加载配置、连数据库、读环境文件,你还得专门包一层async function init(){},然后再自己调用。
现在你可以直接这么写:
const config = await fetchConfig();
startApp(config);示例1:数据库连接
const db = await connectToDatabase();
const users = await db.getUsers();示例2:动态导入
const { default: heavyLib } = await import("./heavy-lib.js");
heavyLib.run();示例3:读取环境配置
const settings = await loadSettingsFromFile();
console.log("应用启动配置:", settings);顶层await的意义在于减少了那种为了迎合语言限制,不得不写的结构性废话。
4. 私有类字段#
JavaScript以前根本谈不上真正意义上的“私有字段”。我们顶多是这样自欺欺人:
class User {
constructor(id) {
this._id = id; // 只是约定,不是保护
}
}现在终于有了真正的私有字段:
class User {
#id;
constructor(id) {
this.#id = id;
}
getId() {
return this.#id;
}
}如果你在类外面直接写user.#id,会是语法错误,不是运行时报错。
示例:私有方法
class Counter {
#count = 0;
#increment() {
this.#count++;
}
increase() {
this.#increment();
}
}很多bug从来都不是因为功能不会写,而是因为“本来不该被碰的东西,被碰了”。
5. .at():相对索引访问
经典面试题:怎么拿数组最后一个元素?
以前:
arr[arr.length - 1];现在:
arr.at(-1);想取最后一个就-1,倒数第二个就-2,语义比算术更直接。
二、ES2023:朝着更安全的数据处理再走一步
6. toSorted()
Array.prototype.sort()会直接修改原数组。以前我们只好养成“先复制再排序”的习惯:
[...arr].sort();现在可以直接写:
const sorted = arr.toSorted();示例:
const numbers = [3, 1, 2];
const sorted = numbers.toSorted();
console.log(numbers); // [3, 1, 2]
console.log(sorted); // [1, 2, 3]这代表JavaScript越来越明显地鼓励一种更稳妥的写法:尽量少突变,多返回新值。
7. toReversed()和toSpliced()
和toSorted()一样,这两个方法延续的是同一个思路:复制,而不是原地修改。
arr.toReversed(); // 反转,不改原数组
arr.toSpliced(2, 1); // 截取,不改原数组示例:
const items = [1, 2, 3, 4];
const reversed = items.toReversed();
const updated = items.toSpliced(1, 1);
console.log(items); // [1, 2, 3, 4]
console.log(reversed); // [4, 3, 2, 1]
console.log(updated); // [1, 3, 4]如果你写React、Vue,或者任何稍微依赖状态不可变的数据流,这种方法真的很香。
8. findLast() / findLastIndex()
以前要找最后一个符合条件的元素,很多人会这么写:
[...arr].reverse().find(fn);现在可以直接写:
arr.findLast(fn);
arr.findLastIndex(fn);示例:
const numbers = [1, 4, 7, 4, 9];
numbers.findLast(n => n > 3); // 9
numbers.findLastIndex(n => n > 3); // 4三、ES2024:更聪明的数据组织与异步控制
9. Object.groupBy()
以前你要把数组按某个字段分组,最常见的写法是reduce():
users.reduce((acc, user) => {
(acc[user.role] ??= []).push(user);
return acc;
}, {});现在简单成了一句:
const grouped = Object.groupBy(users, u => u.role);示例:
const users = [
{ name: "John", role: "admin" },
{ name: "Jane", role: "user" },
{ name: "Bob", role: "admin" }
];
const grouped = Object.groupBy(users, u => u.role);
// { admin: [...], user: [...] }10. Promise.withResolvers()
以前需要在Promise外部控制完成时机时,写法通常长这样:
let resolve;
const promise = new Promise(r => {
resolve = r;
});现在可以直接:
const { promise, resolve, reject } = Promise.withResolvers();示例:
const { promise, resolve } = Promise.withResolvers();
setTimeout(() => resolve("Done!"), 1000);
await promise; // "Done!"11. 可调整大小的ArrayBuffer
以前ArrayBuffer长度一旦创建就固定了。现在可以这样创建:
const buffer = new ArrayBuffer(8, {
maxByteLength: 16
});
buffer.resize(12); // 动态调整大小四、ES2025:函数式风格越来越像主流
12. 迭代器辅助方法(Iterator Helpers)
数组方法每调用一步,往往都会生成一个新的中间数组。数据量大时,这意味着额外的分配和处理成本。
现在,迭代器辅助方法提供了一种更“懒”的处理方式:
const result = iterator
.map(x => x * 10)
.filter(x => x > 80)
.take(5)
.toArray();在真正需要结果之前,它不会急着把所有东西都算出来。
13. 新的Set方法
以前对两个Set做交集、并集、差集,写法通常都不太优雅:
const intersection = new Set([...a].filter(x => b.has(x)));
const union = new Set([...a, ...b]);现在可以直接写:
a.intersection(b); // 交集
a.union(b); // 并集
a.difference(b); // 差集示例:
const a = new Set([1, 2, 3]);
const b = new Set([2, 3, 4]);
a.intersection(b); // Set {2, 3}
a.union(b); // Set {1, 2, 3, 4}
a.difference(b); // Set {1}14. RegExp.escape()
如果用用户输入去动态构造正则,用户输入里只要带了*、.、(、[之类的特殊字符,正则要么失效,要么行为完全变味。
以前大家通常会自己写一个转义工具函数。现在终于有了内建方案:
const regex = new RegExp(RegExp.escape(input));15. Promise.try()
有的函数是同步的会直接throw,有的函数是异步的会返回rejected promise。以前想统一处理这两种错误,代码常常会有点别扭。
现在可以直接:
await Promise.try(() => mightThrow())不管它是正常返回还是中途抛错,最后都会被统一包装进Promise流程里。
示例:
await Promise.try(() => JSON.parse(input))
.then(process)
.catch(handleError);16. Float16Array
JavaScript默认数字是64位浮点数。在某些对性能、内存、传输体积更敏感的场景里,64位其实是有点“奢侈”的。
现在支持更小的半精度浮点数组:
const data = new Float16Array(1000);| 类型 | 位数 |
|---|---|
| Float64Array | 64位 |
| Float32Array | 32位 |
| Float16Array | 16位 |
最后
如果你稍微退一步,不只盯着单个API,而是把这些年JavaScript的变化放在一起看,你会发现它其实在往同一个方向收拢:
更少的突变
更明确的表达
更安全的异步控制
更自然的函数式数据处理
更接近日常开发真实需求的默认能力
JavaScript现在已经不太像从前那样,隔三差五就来一次“很大、很响、很容易上头”的革命了。但回头一看,很多以前自己手动填的坑,原来JavaScript已经悄悄替你填掉了。
本文内容仅供个人学习、研究或参考使用,不构成任何形式的决策建议、专业指导或法律依据。未经授权,禁止任何单位或个人以商业售卖、虚假宣传、侵权传播等非学习研究目的使用本文内容。如需分享或转载,请保留原文来源信息,不得篡改、删减内容或侵犯相关权益。感谢您的理解与支持!