2025年每个JavaScript开发人员都应该知道的一些特性
- 原文链接:https://waspdev.com/articles/2025-04-06/features-that-every-js-developer-must-know-in-2025#some_features_that_every_javascript_developer_should_know_in_2025
- 机器翻译: Gemini 2.5 Pro Preview
- 提示词: 你是资深前端专家,翻译以下文档,并且结合你的前端开发专业知识补充相关的知识点说明,对文章内容进行结构化,以 Markdown 格式返回,文字风格是技术博客,要通俗易懂。
- 翻译理由:这篇文章介绍了一些 JS 新特性,目前已经可以在实际工作中使用了,读完很有收获,确实一些常见的需求,我们常用的写法未必是最佳实践,或者随着时间的推移不断有更好的方法出现。
哈喽,各位奋斗在一线的前端小伙伴们!JavaScript 这门语言一直在高速进化,新特性层出不穷。这也意味着,我们过去的一些编码习惯可能已经悄悄过时,甚至在效率上打了折扣。今天,我们就来盘点一些(不论新旧)但很多开发者可能还没留意到的、非常实用的 JS 特性,一起给自己的技能库充充电!🔋
1. Iterator Helpers (迭代器助手):告别低效的数组链式操作
你有没有写过类似这样的代码:arr.slice(10, 20).filter(el => el < 10).map(el => el + 5)
?这种在同一个数组上连续进行多次转换的操作,看起来很酷,但其实效率不高。为啥?因为每一次 slice
, filter
, map
操作,都会创建一个全新的中间数组。想象一下,如果 arr
是一个包含几十万甚至上百万元素的巨型数组,那内存开销和性能损耗可就相当可观了!😱
为了解决这个问题,JavaScript 最近引入了一系列 迭代器助手(Iterator Helpers) 方法。它们的作用和我们熟悉的数组转换方法(map
, filter
等)类似,但关键区别在于:它们不会每次都创建临时数组,而是创建新的迭代器,这些迭代器基于前一个迭代器进行操作,直到最后才可能生成结果。这是一种更“懒惰”(Lazy Evaluation)且内存友好的方式。
来看看这些得力助手们:
Iterator.prototype.drop(n)
: 跳过迭代器开头的n
个元素,返回一个新的迭代器助手对象。有点像数组的Array.prototype.slice(n)
。Iterator.prototype.take(n)
: 最多获取迭代器开头的n
个元素,返回一个新的迭代器助手对象。类似于数组的Array.prototype.slice(0, n)
。Iterator.prototype.some(callback)
: 和Array.prototype.some()
类似,测试迭代器中是否至少有一个元素满足callback
函数的测试。Iterator.prototype.every(callback)
: 和Array.prototype.every()
类似,测试迭代器中是否所有元素都满足callback
函数的测试。Iterator.prototype.filter(callback)
: 和Array.prototype.filter()
类似,返回一个包含通过callback
测试的元素的新迭代器。Iterator.prototype.find(callback)
: 和Array.prototype.find()
类似,返回迭代器中第一个满足callback
测试的元素。Iterator.prototype.flatMap(callback)
: 和Array.prototype.flatMap()
类似,先映射每个元素,然后将结果展平一层,返回一个新迭代器。Iterator.prototype.forEach(callback)
: 和Array.prototype.forEach()
类似,为迭代器中的每个元素执行一次callback
函数。Iterator.prototype.map(callback)
: 和Array.prototype.map()
类似,返回一个包含经callback
转换后的元素的新迭代器。Iterator.prototype.reduce(callback, initialValue)
: 和Array.prototype.reduce()
类似,对迭代器中的每个元素执行 "reducer" 函数,将其累积成单个值。Iterator.prototype.toArray()
: 这是终结操作之一,它遍历整个迭代器链,并将最终产生的所有值收集到一个新的数组中。
如何创建迭代器?
- 很多内置对象(如
Array
,Map
,Set
,NodeList
等)都有values()
方法可以获取迭代器。 - 可以使用静态方法
Iterator.from()
将一个可迭代对象(比如一个实现了Symbol.iterator
的对象)转换成迭代器助手对象。
回到开头的例子,用迭代器助手优化后就是这样:
// 假设 arr 是一个大数组
const result = arr
.values() // 1. 获取数组迭代器
.drop(10) // 2. 跳过前10个 (返回新迭代器)
.take(10) // 3. 最多取10个 (返回新迭代器)
.filter((el) => el < 10) // 4. 过滤 (返回新迭代器)
.map((el) => el + 5) // 5. 映射 (返回新迭代器)
.toArray(); // 6. 最后生成数组
// 整个过程只在最后 toArray() 时创建了一个新数组,中间步骤几乎没有额外内存分配!
2. Array.prototype.at():优雅地访问数组末尾元素
想获取数组的第 n
个元素,我们通常用 arr[n]
。但如果想获取倒数第一个、倒数第二个元素呢?以前我们得写 arr[arr.length - 1]
、arr[arr.length - 2]
,代码显得有点啰嗦笨拙。
现在有了 Array.prototype.at()
,事情变得简单多了!
- 它接受一个整数作为参数,表示要访问的元素的索引。
- 最酷的是:它支持负数索引!
-1
表示最后一个元素,-2
表示倒数第二个,以此类推。
const colors = ["red", "green", "blue"];
console.log(colors.at(1)); // 'green' (正数索引,和 colors[1] 一样)
console.log(colors.at(-1)); // 'blue' (倒数第一个)
console.log(colors.at(-2)); // 'green' (倒数第二个)
console.log(colors.at(5)); // undefined (索引越界)
console.log(colors.at(-5)); // undefined (索引越界)
// 告别繁琐的 arr[arr.length - n]
console.log(colors[colors.length - 1]); // 'blue' (旧方法)
console.log(colors.at(-1)); // 'blue' (新方法,更简洁!)
这个方法不仅适用于数组,也适用于字符串 (String.prototype.at()
) 和 TypedArrays。代码更简洁,可读性也更高了!
3. Promise.withResolvers():轻松掌控 Promise 的状态
在某些复杂的异步场景下,我们可能需要先创建一个 Promise,然后在稍后的某个时刻、从外部控制它的状态(resolve 或 reject)。以前,我们通常这样做:
let resolveCallback, rejectCallback;
const promise = new Promise((resolve, reject) => {
// 把 resolve 和 reject 函数暴露出来
resolveCallback = resolve;
rejectCallback = reject;
});
// ... 在代码的其他地方 ...
function someAsyncOperation() {
// 异步操作成功
if (success) {
resolveCallback("操作成功的数据");
} else {
// 异步操作失败
rejectCallback(new Error("操作失败"));
}
}
// 使用 promise
promise.then((data) => console.log(data)).catch((err) => console.error(err));
是不是感觉有点绕?代码不够紧凑。好消息是,现在有了 Promise.withResolvers()
这个静态方法,可以更优雅地实现同样的效果:
// 一行代码搞定!
const { promise, resolve, reject } = Promise.withResolvers();
// ... 在代码的其他地方 ...
function someAsyncOperation() {
if (success) {
resolve("操作成功的数据"); // 直接使用解构出来的 resolve
} else {
reject(new Error("操作失败")); // 直接使用解构出来的 reject
}
}
// 使用 promise
promise.then((data) => console.log(data)).catch((err) => console.error(err));
Promise.withResolvers()
直接返回一个包含 promise
、resolve
函数和 reject
函数的对象,代码瞬间清爽了很多!这个特性对于实现一些需要外部信号来完成的异步流程(比如等待某个事件触发)特别有用。
4. String.prototype.replace() / replaceAll() 的回调函数:强大的动态替换
replace()
和 replaceAll()
用来替换字符串中的子串,这我们都知道。但你知道吗?它们的第二个参数不仅可以是替换字符串,还可以是一个回调函数!这可就厉害了,它赋予了我们进行动态、上下文相关替换的能力。
这个回调函数会接收到匹配到的子串(以及其他信息,如捕获组、索引位置等),然后函数的返回值将作为替换内容。
来看个例子,给句子中所有出现的 "NUMBER" 动态编号:
let counter = 0;
const text =
"Found NUMBER item, then another NUMBER item, and finally the last NUMBER.";
const result = text.replaceAll("NUMBER", (match) => {
// match 就是匹配到的 "NUMBER"
counter++;
return `${match}=${counter}`; // 返回动态生成的替换内容
});
console.log(result);
// 输出: "Found NUMBER=1 item, then another NUMBER=2 item, and finally the last NUMBER=3."
这个特性非常强大且高效:
- 动态替换:可以根据匹配到的内容、上下文、甚至外部状态来决定替换为什么。
- 一次遍历:
replaceAll
配合回调,只需要一次遍历就能完成所有动态替换,相比多次调用replace
性能更好,内存占用也更低。 - 复杂逻辑:可以在回调函数里执行更复杂的逻辑,比如查表、计算等。
想了解回调函数的详细参数,可以查阅 MDN 文档。
5. 变量交换:更优雅的 ES6 方式
交换两个变量的值,经典的“三杯水”方法是使用一个临时变量:
let a = 1,
b = 2;
console.log(a, b); // 1, 2
const temp = a;
a = b;
b = temp;
console.log(a, b); // 2, 1
虽然可行,但不够简洁。在 ES6 中,我们可以利用数组解构赋值 (Destructuring Assignment) 来实现更优雅的变量交换:
let a = 1,
b = 2;
console.log(a, b); // 1, 2
[a, b] = [b, a]; // 一行代码,搞定!
console.log(a, b); // 2, 1
是不是瞬间感觉代码逼格高了不少?这利用了等号右侧先创建一个临时数组 [b, a]
(即 [2, 1]
),然后左侧通过解构赋值,将这个临时数组的第一个元素赋给 a
,第二个元素赋给 b
。简洁、直观、高效!
6. structuredClone():更靠谱的深拷贝选择
深拷贝(Deep Copy)对象是前端开发中常见的需求。很多人习惯使用 JSON.stringify()
和 JSON.parse()
的组合拳来实现:const clonedObj = JSON.parse(JSON.stringify(originalObj))
。这种方法简单粗暴,但在很多情况下并不可靠,甚至会出错!
JSON.stringify()
/ JSON.parse()
深拷贝的“坑”:
- 丢失或转换特定值类型:
undefined
、Function
、Symbol
类型的值,在JSON.stringify
时会被忽略(如果在对象中)或转换为null
(如果在数组中)。NaN
、Infinity
、-Infinity
会被转换为null
。Date
对象会被转换为 ISO 格式的字符串,parse
后不会自动变回Date
对象。RegExp
正则表达式对象会被转换为空对象{}
。Map
、Set
会被转换为空对象{}
。- 不支持
BigInt
类型,会直接抛出TypeError
。
- 无法处理循环引用:如果对象内部存在循环引用(例如
obj.self = obj
),JSON.stringify()
会抛出TypeError
。const obj = {};
obj.selfReference = obj;
// console.log(JSON.stringify(obj)); // Uncaught TypeError: Converting circular structure to JSON - 性能问题:对于非常大的或嵌套很深的对象,先序列化为庞大的 JSON 字符串,再解析回来,性能开销和内存占用都比较大。
推荐使用 structuredClone()
幸运的是,现代浏览器和 Node.js 环境都内置了 structuredClone()
API。它专门用于执行深拷贝,并且能够正确处理许多 JSON
方法无法处理的情况。
structuredClone()
的优势:
- 支持更多类型:能正确克隆
Date
,RegExp
,Map
,Set
,Blob
,File
,ImageData
,ArrayBuffer
,TypedArray
等多种类型。 - 自动处理循环引用:无需担心循环引用问题,
structuredClone()
会正确地在克隆对象中重建这些引用关系。 - 性能通常更好:底层使用结构化克隆算法(与 Web Workers
postMessage
、IndexedDB 存储对象时使用的算法相同),通常比JSON
序列化/反序列化更快,内存效率也更高。
const original = {
date: new Date(),
regex: /hello/g,
map: new Map([["a", 1]]),
set: new Set([1, 2]),
undef: undefined,
num: 123,
// BigInt 需要注意,虽然 structuredClone 支持,但 JSON 不支持
// bigNum: 123n, // 如果要用 JSON 序列化,这行会报错
};
original.circular = original; // 添加循环引用
// 使用 structuredClone
const cloned = structuredClone(original);
console.log(cloned !== original); // true (是不同的对象引用)
console.log(cloned.date instanceof Date); // true (仍然是 Date 对象)
console.log(cloned.regex instanceof RegExp); // true (仍然是 RegExp 对象)
console.log(cloned.map instanceof Map); // true (仍然是 Map 对象)
console.log(cloned.set instanceof Set); // true (仍然是 Set 对象)
console.log(cloned.undef); // undefined (undefined 被保留了)
console.log(cloned.circular === cloned); // true (循环引用也正确克隆了)
// 对比 JSON 方法 (会丢失信息或改变类型)
// const jsonCloned = JSON.parse(JSON.stringify(original)); // 如果包含 BigInt 会报错
// console.log(jsonCloned.date); // 变成字符串了
// console.log(jsonCloned.regex); // 变成 {}
// console.log(jsonCloned.map); // 变成 {}
// console.log(jsonCloned.set); // 变成 {}
// console.log(jsonCloned.undef); // undefined 丢失了 (属性直接没了)
// console.log(JSON.stringify(original)); // 如果有循环引用,这步就报错了
注意:structuredClone()
也不是万能的,它无法克隆函数 (Function)、错误对象 (Error) 和 DOM 节点。但对于绝大多数数据对象的深拷贝场景,structuredClone()
是目前最优、最可靠的选择。
7. Tagged Templates (标签模板):给模板字符串加个“处理器”
我们对模板字符串(用反引号 `
包裹的字符串)应该很熟悉了,它允许我们方便地嵌入表达式 ${expression}
。但你可能没太注意过标签模板 (Tagged Templates)。
标签模板允许你用一个函数来“解析”模板字符串。这个函数(称为“标签函数”)会接收到模板字符串的各个部分作为参数,然后你可以对它们进行处理,最后返回处理后的结果。
如何工作?
标签函数的第一个参数是一个数组,包含了模板字符串中被 ${...}
分隔开的静态文本部分。
后续的参数则是模板字符串中每个 ${...}
表达式的计算结果。
这有什么用呢?它非常适合用来对模板字符串进行自动转换、校验或处理。
例如:自动 HTML 转义
假设我们想创建一个标签函数 escapeHtml
,它能自动对模板字符串中的插值(${...}
里的内容)进行 HTML 转义,防止 XSS 攻击:
function escapeHtml(strings, ...values) {
let output = strings[0]; // 先获取第一个静态文本片段
for (let i = 0; i < values.length; i++) {
// 对每个表达式的值进行处理 (这里是 HTML 转义)
const value = values[i];
const escapedValue = String(value) // 确保是字符串
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
output += escapedValue; // 拼接转义后的值
output += strings[i + 1]; // 拼接下一个静态文本片段
}
return output;
}
const userInput = '<script>alert("xss")</script>';
const safeHtml = escapeHtml`<p>用户输入内容:${userInput}</p>`;
console.log(safeHtml);
// 输出: "<p>用户输入内容:<script>alert("xss")</script></p>"
// 用户输入的恶意脚本被成功转义了!
原文中的例子 (使用 innerText
和 innerHTML
来转义,也是一种技巧,但不如直接替换特殊字符通用):
// 原文示例的另一种实现思路
function escapeHtmlViaDOM(strings, ...args) {
const div = document.createElement("div"); // 仅在浏览器环境有效
let output = strings[0];
for (let i = 0; i < args.length; ++i) {
div.innerText = args[i]; // 利用 innerText 的自动转义特性
output += div.innerHTML; // 读取转义后的 HTML
output += strings[i + 1];
}
div.remove(); // 清理 DOM 元素
return output;
}
const maliciousInput = "<br>";
console.log(escapeHtmlViaDOM`<p>Here is some code: ${maliciousInput}</p>`);
// 输出: "<p>Here is some code: <br></p>"
标签模板非常强大,常见应用场景包括:
- HTML/SQL 转义:如上例所示,防止注入攻击。
- 国际化 (i18n):根据语言环境自动替换占位符。
- 样式化组件 (Styled Components):像 React 中的 styled-components 库就重度使用了标签模板来写 CSS in JS。
- 创建领域特定语言 (DSL):比如用于生成特定格式的查询语句。
8. WeakMap / WeakSet:不阻止垃圾回收的“弱”集合
除了我们常用的 Map
和 Set
,JavaScript 还提供了它们的“弱”版本:WeakMap
和 WeakSet
。它们与 Map
和 Set
类似,但有几个关键区别和特性:
- 键必须是对象:
WeakMap
的键 (key) 和WeakSet
的值 (value) 只能是对象,不能是原始类型值(如string
,number
,boolean
,symbol
,bigint
,null
,undefined
)。 - 弱引用 (Weak Reference):这是它们的核心特性。
WeakMap
和WeakSet
对其存储的对象键(或值)是弱引用。这意味着,如果一个对象只被WeakMap
或WeakSet
引用,而没有其他任何强引用指向它,那么这个对象就可以被垃圾回收机制 (Garbage Collection, GC) 回收掉。回收发生时,对应的键值对(或值)也会自动从WeakMap
/WeakSet
中移除。 - 不可迭代:正因为垃圾回收可能随时移除成员,
WeakMap
和WeakSet
不支持迭代(没有keys()
,values()
,entries()
,forEach
方法),也无法获取size
属性。这是为了防止在迭代过程中成员突然消失导致的不确定性。
为什么需要它们?
主要用途是将数据与对象关联起来,同时又不阻止该对象被垃圾回收。这在某些场景下非常有用,可以避免内存泄漏。
示例:
const weakSet = new WeakSet();
const weakMap = new WeakMap();
let obj1 = { id: 1 };
let obj2 = { id: 2 };
weakSet.add(obj1);
weakMap.set(obj2, "一些与 obj2 关联的数据");
console.log(weakSet.has(obj1)); // true
console.log(weakMap.get(obj2)); // '一些与 obj2 关联的数据'
// 现在,我们移除对 obj1 和 obj2 的所有其他强引用
obj1 = null;
obj2 = null;
// 在将来的某个时刻,垃圾回收机制运行时...
// obj1 和 obj2 因为不再被强引用,它们占用的内存会被回收。
// 同时,它们也会自动从 weakSet 和 weakMap 中移除。
// 注意:我们无法直接验证它们是否已被移除,因为 WeakMap/WeakSet 不可迭代。
// 但我们可以确信,它们不会阻止 obj1 和 obj2 被回收。
// console.log(weakSet.has(obj1)); // 此时 obj1 已经是 null 了,查询无意义
// console.log(weakMap.get(obj2)); // 此时 obj2 已经是 null 了,查询无意义
常见用例:
- 缓存对象元数据:为一个对象存储一些计算成本较高的元数据。当对象不再使用并被回收时,关联的元数据也自动被清理,无需手动管理。
- 存储私有数据:在类外部(或模拟私有成员)为类的实例关联一些数据,而不会干扰实例本身的生命周期。
- 跟踪 DOM 节点状态:比如记录某个 DOM 元素是否已被处理,当该 DOM 元素从文档中移除并被回收时,
WeakMap
中的记录也自动消失。
如果你需要在不影响对象生命周期的情况下,给对象附加一些信息,WeakMap
和 WeakSet
就是你的不二之选。
9. Set 集合操作:轻松实现交、并、差集
Set
对象存储唯一值,本身就很有用。最近,JavaScript 又为 Set
增加了几个进行集合运算的方法,使得处理集合之间的关系变得异常简单和高效。再也不用自己写循环和判断逻辑啦!
这些方法都会返回一个新的 Set
,而不会修改原始的 Set
。
set1.difference(set2)
(差集): 返回一个新 Set,包含所有在set1
中但不在set2
中的元素。const set1 = new Set([1, 2, 3, 4]);
const set2 = new Set([3, 4, 5, 6]);
console.log(set1.difference(set2)); // Set(2) { 1, 2 }set1.intersection(set2)
(交集): 返回一个新 Set,包含所有同时存在于set1
和set2
中的元素。const set1 = new Set([1, 2, 3, 4]);
const set2 = new Set([3, 4, 5, 6]);
console.log(set1.intersection(set2)); // Set(2) { 3, 4 }set1.union(set2)
(并集): 返回一个新 Set,包含所有在set1
或set2
中(或两者皆有)的元素。const set1 = new Set([1, 2, 3, 4]);
const set2 = new Set([3, 4, 5, 6]);
console.log(set1.union(set2)); // Set(6) { 1, 2, 3, 4, 5, 6 }set1.symmetricDifference(set2)
(对称差集): 返回一个新 Set,包含所有只存在于set1
或set2
之一中,但不同时存在于两者中的元素。(即并集减去交集)。const set1 = new Set([1, 2, 3, 4]);
const set2 = new Set([3, 4, 5, 6]);
console.log(set1.symmetricDifference(set2)); // Set(4) { 1, 2, 5, 6 }set1.isDisjointFrom(set2)
(是否不相交): 返回一个布尔值,判断set1
和set2
是否没有共同元素。const set1 = new Set([1, 2, 3, 4]);
const set2 = new Set([3, 4, 5, 6]);
const set3 = new Set([5, 6]);
console.log(set1.isDisjointFrom(set2)); // false (因为有 3, 4)
console.log(set1.isDisjointFrom(set3)); // true (没有共同元素)set1.isSubsetOf(set2)
(是否为子集): 返回一个布尔值,判断set1
中的所有元素是否都存在于set2
中。const set1 = new Set([1, 2]);
const set2 = new Set([1, 2, 3, 4]);
const set3 = new Set([3, 4]);
console.log(set1.isSubsetOf(set2)); // true
console.log(set3.isSubsetOf(set1)); // falseset1.isSupersetOf(set2)
(是否为超集): 返回一个布尔值,判断set1
是否包含set2
中的所有元素。const set1 = new Set([1, 2, 3, 4]);
const set2 = new Set([1, 2]);
const set3 = new Set([5, 6]);
console.log(set1.isSupersetOf(set2)); // true
console.log(set1.isSupersetOf(set3)); // false
这些集合操作方法极大地简化了处理数据集相关的逻辑,比如比较用户权限、筛选数据、查找差异等场景,代码更易读,性能也通常优于手动实现。
总结
JavaScript 的世界日新月异,不断有新的特性和语法糖涌现,旨在让我们写出更高效、更简洁、更健壮的代码。今天介绍的这些特性,有些是近年的新秀(如 Iterator Helpers, Set Operations),有些是虽老但可能被忽视的实用技巧(如 replace
回调, 变量解构交换)。
作为前端开发者,持续学习、拥抱变化是我们的必备素养。希望这篇解读能帮助你更好地理解和运用这些 JavaScript 特性,提升你的开发效率和代码质量!下次遇到类似场景,不妨试试这些新“武器”吧!🚀