JavaScript 不断发展,新的特性不断被引入。这使得一些旧的编码实践变得过时,甚至效率更低。以下是大多数开发者可能不知道的一些重要特性(新旧都有)。
迭代器辅助方法
你是否曾经对同一个数组执行过多次链式数组转换?例如,像这样的代码:arr.slice(10, 20).filter(el => el < 10).map(el => el + 5)
。这非常低效,因为每次转换都需要分配一个新的数组。想象一下,如果你对一个很大的数组(>500K 元素)这么做......这就是为什么 JavaScript 最近引入了迭代器方法。它们的工作方式类似于常规的数组转换方法,但不会创建临时数组,而是创建新的迭代器来迭代其他迭代器。以下是这些方法的列表:
Iterator.prototype.drop()
:返回一个新的迭代器辅助对象,跳过此迭代器开头的指定数量的元素。它大致相当于常规数组中的Array.prototype.slice(n)
。Iterator.prototype.take()
:返回一个新的迭代器辅助对象,从该迭代器的开头最多取指定数量的元素。它大致相当于常规数组中的Array.prototype.slice(0, n)
。Iterator.prototype.some()
:类似于Array.prototype.some()
。它测试迭代器生成的元素中是否至少有一个通过了提供的函数实现的测试。Iterator.prototype.every()
:类似于Array.prototype.every()
。它测试迭代器生成的所有元素是否都通过了提供的函数实现的测试。Iterator.prototype.filter()
:类似于Array.prototype.filter()
。返回一个对过滤值的迭代器。Iterator.prototype.find()
:类似于Array.prototype.find()
。返回迭代器生成的第一个满足提供的测试函数的元素。Iterator.prototype.flatMap()
:类似于Array.prototype.flatMap()
。返回一个对展平值的迭代器。Iterator.prototype.forEach()
:类似于Array.prototype.forEach()
。它对迭代器生成的每个元素执行一次提供的函数。Iterator.prototype.map()
:类似于Array.prototype.map()
。返回一个通过映射函数转换后的值的迭代器。Iterator.prototype.reduce()
:类似于Array.prototype.reduce()
。它对迭代器生成的每个元素执行用户提供的"归并器"回调函数,并传入前一个元素计算的返回值。Iterator.prototype.toArray()
:创建一个用已生成的值填充的数组。
创建可迭代对象的常见方法是通过静态方法 Iterator.from()
,以及 Array
、NodeList
、Set
和许多其他容器的 values()
方法。
因此,给定示例的转换链的更节省内存的版本将是:arr.values().drop(10).take(10).filter(el => el < 10).map(el => el + 5).toArray()
。
需要注意的是,这是一个相对较新的特性,最后一个支持此特性的主流浏览器是 Safari。它从 2025 年 3 月 31 日开始支持,所以最好再等几个月。
数组的 at()
方法
Array.prototype.at()
是一种访问第 n 个元素的替代方法。它的一个好处是它还支持负索引,它将从最后一个元素开始计数。例如 [10,20,30].at(-1)
将返回 30,[10,20,30].at(-2)
将返回 20 等等。这种负索引使得访问最后一个元素变得更加容易。以前,你不得不编写这种丑陋的样板代码 arr[arr.length - 1]
。
Promise.withResolvers()
你是否曾经编写过这样的代码以便稍后使用承诺解析器?
javascript
let resolve, reject;
const promise = new Promise((resolver, rejector) => {
resolve = resolver;
reject = rejector;
});
// 稍后使用 promise、resolve 和 reject
// ......⎘
是不是很繁琐?幸运的是,现在这已经成为过去,因为 JavaScript 现在支持 Promise.withResolvers()
。所以你可以这样写:
javascript
const { promise, resolve, reject } = Promise.withResolvers();
// 稍后使用 promise、resolve 和 reject
// ......⎘
String.prototype.replace()
/ String.prototype.replaceAll()
的回调
这是一件老事情,但许多开发者不知道,你可以为 String.prototype.replace()
或 String.prototype.replaceAll()
的第二个参数传递一个回调函数,而不是字符串。例如:
javascript
let counter = 0;
console.log("NUMBER, NUMBER, NUMBER".replaceAll("NUMBER", (match) => match + "=" + (++counter))) // NUMBER=1, NUMBER=2, NUMBER=3⎘
这是一个非常强大的功能,它允许在一次遍历中进行多次替换。从性能和内存角度来看,这非常高效。更多详情请查阅文档。
交换变量
这是另一个老生常谈的问题。人们经常这样交换变量:
javascript
let a = 1, b = 2;
console.log(a, b); // 1, 2
const temp = a;
a = b;
b = temp;
console.log(a, b); // 2, 1⎘
你应该这样做:
javascript
let a = 1, b = 2;
console.log(a, b); // 1, 2
[a, b] = [b, a];
console.log(a, b); // 2, 1⎘
structuredClone()
如今浏览器支持 structuredClone()
API。这是一个非常方便的函数,用于深度复制大多数常规对象。然而,人们经常使用 JSON.stringify()
和 JSON.parse()
来深度复制对象,而没有考虑是否合适。
在使用 JSON.stringify()
/ JSON.parse()
时,可能会出错的地方如下:
JSON.stringify()
不支持某些值,例如NaN
或undefined
。它们可能会被跳过或转换为null
。对于某些数据类型,如bigint
,它甚至会抛出异常。JSON.stringify()
无法处理包含循环引用的对象:
javascript
const obj = {};
obj.selfReference = obj;
console.log(JSON.stringify(obj)); // 异常⎘
虽然通常没有前两个问题严重,但不得不说,对于较大的对象,它效率不高。它速度慢且浪费大量内存。
应该尽可能优先使用 structuredClone()
。structuredClone()
还会自动处理自引用 / 循环结构。
javascript
const obj = {};
obj.selReference = obj;
const clonedObj = structuredClone(obj);
console.log(obj === clonedObj); // false,因为这是一个克隆对象,具有不同的内存地址
console.log(clonedObj.selReference === clonedObj); // true,因为它具有与 obj 相同的结构(与 obj 同构,即作为图)⎘
关于对象克隆和比较的更高级信息,请阅读这篇文章。
标签化模板
我们大多数人都熟悉模板字面量(),但许多人不知道标签化模板。标签化模板允许你用一个函数解析模板字面量。标签函数的第一个参数包含一个字符串值的数组。其余参数与表达式相关。当你要对插值值(甚至整个字符串)(
${... here ...}`` 中的值)进行一些自动转换时,标签化模板非常有用。
例如,我们想在插值时自动转义 HTML 文本:
javascript
function escapeHtml(strings, ...arguments) {
const div = document.createElement("div");
let output = strings[0];
for (let i = 0; i < arguments.length; ++i) {
div.innerText = arguments[i];
output += div.innerHTML;
output += strings[i + 1];
}
return output;
}
console.log(escapeHtml`<br> ${'<br>'}`); // <br> <br>⎘
WeakMap
/ WeakSet
除了 Map
和 Set
,JavaScript 还支持 WeakMap
和 WeakSet
。WeakMap
和 WeakSet
与 Map
和 Set
类似,只是它们不允许其键使用原始值,而且它们没有迭代器。之所以这样设计,是因为当你丢失了指向键的所有引用时,键以及可能的关联值必须有可能从映射 / 集合中释放并被垃圾回收。
javascript
const set = new WeakSet();
const map = new WeakMap();
{
const key1 = new Date();
const key2 = new Date();
console.log(set.has(key1)); // false
set.add(key1);
console.log(set.has(key1)); // true
console.log(map.get(key2)); // undefined
map.set(key2, 10);
console.log(map.get(key2)); // 10
}
// 在这里我们丢失了对 key1 和 key2 的引用,因此稍后键和值将被垃圾回收⎘
如果想将某物与对象关联起来而不产生任何副作用,就使用 `WeakMap` 或 `WeakSet`。
### 集合操作
最近 JavaScript 增加了对集合对象的布尔运算的支持。以下是布尔运算的列表:
1. `Set.prototype.difference()`:返回一个新集合,包含此集合中的元素,但不包含给定集合中的元素。
```javascript
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}⎘
Set.prototype.intersection()
:返回一个新集合,包含此集合和给定集合中都有的元素。
javascript
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}⎘
Set.prototype.union()
:返回一个新集合,包含此集合和给定集合中任一或两者都有的元素。
javascript
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}⎘
Set.prototype.symmetricDifference()
:返回一个新集合,包含此集合和给定集合中任一或两者都有的元素。
javascript
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}⎘
Set.prototype.isDisjointFrom()
:返回一个布尔值,指示此集合是否与给定集合没有共同元素。
javascript
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
console.log(set1.isDisjointFrom(set3)); // true⎘
Set.prototype.isSubsetOf()
:返回一个布尔值,指示此集合的所有元素是否都在给定集合中。
javascript
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.isSubsetOf(set2)); // false
console.log(set3.isSubsetOf(set2)); // true⎘
Set.prototype.isSupersetOf()
:返回一个布尔值,指示给定集合的所有元素是否都在此集合中。
javascript
const set1 = new Set([1,2,3,4]);
const set2 = new Set([3,4,5,6]);
const set3 = new Set([5,6]);
console.log(set2.isSupersetOf(set1)); // false
console.log(set2.isSupersetOf(set3)); // true