1.Iterator helpers
开发者经常会对同一个数组执行多次链式转换,例如:
1 | const arr = []; |
这种方式非常低效,因为每次转换都需要分配一个新数组。JS 引入了迭代器方法,其工作原理与常规数组转换类似。不同之处在于其不会创建临时数组,而是创建新的迭代器,这些迭代器会在其他迭代器上进行迭代。
- Iterator.prototype.drop():返回一个新的 Iterator Helper 对象,该对象会跳过此迭代器开头指定数量的元素,其作用大致与常规数组的 Array.prototype.slice(n) 相同
- Iterator.prototype.take():返回一个新的 Iterator Helper 对象,该对象从此迭代器的开头获取指定数量的元素,其作用与常规数组的 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(),其返回迭代器生成的第一个满足所提供测试函数的元素,否则返回 undefined
1 | function* fibonacci() { |
- Iterator.prototype.flatMap():类似于 Array.prototype.flatMap(),其返回一个基于扁平化值的迭代器
1 | // 展平可迭代对象 |
该方法可以避免创建任何 map 内容的临时副本。但需要注意的是,数组 [map1, map2] 必须先通过. values() 转换为迭代器,因为 Array.prototype.flatMap() 只能展平数组,而不能展平可迭代对象。
- Iterator.prototype.forEach():类似于 Array.prototype.forEach(),其对迭代器生成的每个元素执行一次提供的函数
- Iterator.prototype.map():类似于 Array.prototype.map(),其返回一个由映射函数转换后的值的迭代器
- Iterator.prototype.reduce():类似于 Array.prototype.reduce(),其对迭代器生成的每个元素执行用户提供的 reducer 回调函数,并传入前一个元素计算的返回值
1 | // 该示例创建迭代器,该迭代器产生斐波那契数列中的项,然后对前十个项求和 |
- Iterator.prototype.toArray():使用填充的生成值创建一个数组
创建可迭代对象的常用方法是通过静态方法 Iterator.from() 以及 Array、NodeList、Set 和许多其他容器的 values() 方法。因此,上面转换链示例的更节省内存的版本是:
1 | const arr = []; |
需要注意的是,这是一个相对较新的功能,最后一个开始支持此功能的主流浏览器是 Safari,其从 2025 年 3 月 31 日才开始支持。
2. 数组的 at() 方法
Array.prototype.at() 是访问第 n 个元素的另一种方法,其还支持负索引,即从最后一个元素开始计数。
例如,[10,20,30].at(-1) 将返回 30,[10,20,30].at(-2) 将返回 20 等等。这种负索引使得访问最后一个元素变得非常容易。在此之前,开发者则必须编写丑陋的样板代码 arr[arr.length - 1]。
3.Promise.withResolvers()
在异步调用场景,开发者经常会编写如下代码:
1 | let resolve, reject; |
但是,借助于最新的 Promise.withResolvers(),开发者可以轻松实现同样的功能。
1 | const {promise, resolve, reject} = Promise.withResolvers(); |
Promise.withResolvers() 静态方法返回一个对象,其中包含一个新的 Promise 对象和两个用于 resolve 或 reject 的函数,对应于传递给 Promise() 构造函数执行器的两个参数。
4.String.prototype.replace() / String.prototype.replaceAll() 回调
这是个老生常谈的问题,很多开发者不知道 String.prototype.replace() 或
String.prototype.replaceAll() 的第二个参数可以传入回调函数而不仅仅是字符串。例如:
1 | let counter = 0; |
replace() 和 replaceAll() 是一个非常强大的功能,其允许一次性完成多次替换。而且,从性能和内存角度来看都非常高效。
5. 交换变量的最新方法
很多开发者经常使用下面的方式来交换变量:
1 | let a = 1, |
现在 JavaScript 引擎提供了更加简单的方式来实现同样的功能:
1 | let a = 1, |
6. 结构化克隆 structuredClone()
大部分浏览器都已经支持 structuredClone() API,其可以深度复制大多数常规对象从而替换 JSON.stringify() 和 JSON.parse() 进行深度复制的功能。因为后者在很多场景可能并不符合预期:
- JSON.stringify() 不支持某些值,例如: NaN 或 undefined,这些值会被跳过或转换为 null。同时,对于某些数据类型,例如: BigInt,该方法甚至会抛出异常。
- JSON.stringify() 无法处理包含循环引用的对象:
1 | const obj = {}; |
- 对于较大的对象来说,该方法效率不高且浪费大量内存
因此总体来看,开发者应尽可能优先使用 structuredClone(),其还会循环引用对象。
1 | const obj = {}; |
7.Tagged templates
大多数开发者都熟悉模板字面量 “`”,但很多人并不知道标记模板,标记模板允许开发者使用函数解析模板字面量。标记函数的第一个参数包含一个字符串值数组,其余参数与表达式相关。当想对插值,甚至整个字符串,进行一些自动转换时,标记模板非常有用。
1 | `string text` |
例如,下面代码示例会在进行插值时自动转义 HTML 文本:
1 | function escapeHtml(strings, ...arguments) { |
8.WeakMap / WeakSet
除了 Map 和 Set 之外,JavaScript 还支持 WeakMap 和 WeakSet。
WeakMap 和 WeakSet 与 Map 和 Set 类似,不同之处在于其不允许将原始值作为键,并且不提供迭代器。这样做的原因是,当指向某个键的所有引用丢失时,该键以及可能关联的值必须能够从 Map/Set 中释放并被垃圾回收。
1 | const set = new WeakSet(); |
因此,如果开发者希望将某事物与某个对象关联而不产生副作用,可以考虑使用 WeakMap 或 WeakSet。
9.Set 操作
最近,JavaScript 增加了对 Set 对象的布尔运算支持。
Set.prototype.difference()
集合差运算:
1 | const set1 = new Set([1, 2, 3, 4]); |
Set.prototype.intersection()
执行集合交运算:
1 | const set1 = new Set([1, 2, 3, 4]); |
Set.prototype.union()
执行集合并操作:
1 | const set1 = new Set([1, 2, 3, 4]); |
Set.prototype.symmetricDifference()
除去两个集合的公共元素:
1 | const set1 = new Set([1, 2, 3, 4]); |
Set.prototype.isDisjointFrom()
返回一个布尔值,指示该集合与给定集合没有共同元素:
1 | const set1 = new Set([1, 2, 3, 4]); |
Set.prototype.isSubsetOf()
返回一个布尔值,指示该集合的所有元素是否都在给定集合中:
1 | const set1 = new Set([1, 2, 3, 4]); |
Set.prototype.isSupersetOf()
返回一个布尔值,指示给定集合的所有元素都在此集合中:
1 | const set1 = new Set([1, 2, 3, 4]); |