JavaScript一直在不断进化,新特性层出不穷,这往往让旧的编码实践变得过时,甚至效率更低。下面是一些大多数开发者可能不知道的重要特性(既有新的也有旧的)。
目录
- 迭代器辅助方法
- Array.at()方法
- Promise.withResolvers()
- String.prototype.replace()/replaceAll(…
- 变量交换
- structuredClone()
- 标签模板
- WeakMap/WeakSet
- Set操作
迭代器辅助方法
你有没有对同一个数组执行过多次链式转换?例如这样:
arr.slice(10, 20).filter(el => el < 10).map(el => el + 5)
这真的很低效,因为每次转换都要分配一个新数组。想象一下,如果你对超大型数组(> 500K元素)这样做...这就是为什么JS最近引入了迭代器方法。它们的工作方式与常规数组转换方法类似,但不会创建临时数组,而是创建在其他迭代器上迭代的新迭代器。
以下是这些方法的列表:
- 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()类似。它对迭代器生成的每个元素执行用户提供的"reducer"回调函数,传入计算前一个元素的返回值。 - 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日开始支持,所以最好至少等待几个月再使用。
Array.at()方法
Array.prototype.at()是访问第n个元素的另一种方式。最酷的是它还支持负索引,从最后一个元素开始计数。
例如:
[10, 20, 30].at(-1) // 返回 30
[10, 20, 30].at(-2) // 返回 20
这种负索引使得访问最后一个元素变得更加容易。在这之前,你必须编写这种丑陋的样板代码:arr[arr.length - 1]。
Promise.withResolvers()
你有没有为了以后使用promise解析器而写过这样的代码?
let resolve, reject;
const promise = new Promise((resolver, rejector) => {
resolve = resolver;
reject = rejector;
});
// 稍后使用promise、resolve和reject
// ......
很臃肿,不是吗?幸运的是,这现在已经成为过去,因为JS现在支持Promise.withResolvers()。所以你可以这样写:
const { promise, resolve, reject } = Promise.withResolvers();
// 稍后使用promise、resolve和reject
// ......
String.prototype.replace()/replaceAll()回调
这是一个老特性,但许多开发者不知道,你可以为String.prototype.replace()或String.prototype.replaceAll()的第二个参数传递一个回调函数,而不是字符串。替换字符串将是回调函数返回的值。例如:
let counter = 0;
console.log("NUMBER, NUMBER, NUMBER".replaceAll("NUMBER", (match) => match + "=" + (++counter)))
// NUMBER=1, NUMBER=2, NUMBER=3
这是一个非常强大的功能,它允许你只需要一次遍历就可以进行多次替换。从性能和内存角度来看都非常高效。
变量交换
另一个老特性。人们经常这样交换变量:
let a = 1, b = 2;
console.log(a, b); // 1, 2
const temp = a;
a = b;
b = temp;
console.log(a, b); // 2, 1
试试这样做吧:
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()不能处理包含循环引用的对象:const obj = {}; obj.selfReference = obj; console.log(JSON.stringify(obj)); // 抛出异常 - 效率低下:虽然通常不如前两个严重,但我必须说,对于较大的对象来说,它效率不高。它速度慢,并且浪费大量内存。
应该尽可能优先使用structuredClone()。structuredClone()还会自动处理自引用/循环结构。
const obj = {};
obj.selfReference = obj;
const clonedObj = structuredClone(obj);
console.log(obj === clonedObj);
// false,因为它是一个具有不同内存地址的克隆对象
console.log(clonedObj.selfReference === clonedObj);
// true,因为它与obj具有相同的结构
标签模板
我们大多数人都熟悉模板字面量(`),但许多人不知道标签模板。标签模板允许你使用函数解析模板字面量。标签函数的第一个参数包含一个字符串值数组。其余参数与表达式相关。当你想对插值值(或整个字符串)进行一些自动转换时,标签模板非常有用。
例如,我们希望在进行插值时自动转义HTML文本:
function escapeHtml(strings, ...args) {
const div = document.createElement("div");
let output = strings[0];
for (let i = 0; i < args.length; ++i) {
div.innerText = args[i];
output += div.innerHTML;
output += strings[i + 1];
}
return output;
}
console.log(escapeHtml`<br> ${'<br>'}`); // <br> &lt;br&gt;
WeakMap / WeakSet
除了Map和Set之外,JavaScript还支持WeakMap和WeakSet。WeakMap和WeakSet与Map和Set类似,除了它们不允许将原始值作为键,并且它们缺少迭代器。这样做是因为当你失去所有指向键的引用时,键以及可能的关联值必须有可能从映射/集合中释放并被垃圾回收。
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。
Set操作
最近,JavaScript为Set对象添加了布尔操作支持。以下是布尔操作的列表:
-
Set.prototype.difference():返回一个新集合,包含此集合中但不在给定集合中的元素。
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():返回一个新集合,包含此集合和给定集合中都有的元素。
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():返回一个新集合,包含此集合或给定集合中的所有元素(或两者都有)。
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():返回一个新集合,包含此集合或给定集合中但不是两者都有的元素。
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():返回一个布尔值,表示此集合是否与给定集合没有共同元素。
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():返回一个布尔值,表示此集合的所有元素是否都在给定集合中。
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():返回一个布尔值,表示给定集合的所有元素是否都在此集合中。
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
结语
JavaScript的这些特性可以帮助你编写更简洁、更高效、更可读的代码。无论你是新手还是经验丰富的开发者,都值得花时间学习这些特性,它们将使你的前端开发工作更加轻松愉快!
你最喜欢哪个特性?欢迎在评论区分享你的看法!👇