每个 JavaScript 开发者在 2025 年应该了解的一些特性
Some features that every JavaScript developer should know in 2025

原始链接: https://waspdev.com/articles/2025-04-06/features-that-every-js-developer-must-know-in-2025

本文发表于2025年6月4日,重点介绍了现代JavaScript特性和高效的编码实践。 迭代器辅助函数通过避免使用临时数组来优化链式数组转换。`at()` 方法提供了一种简洁的方式来访问数组元素,包括用于从末尾访问的负索引。`Promise.withResolvers()` 简化了Promise的创建,并提供了 readily available 的 resolve/reject 函数。 利用`replace()`/`replaceAll()`中的回调函数可以进行强大的一遍式字符串操作。使用解构赋值 `[a, b] = [b, a]` 交换变量。由于其对数据类型、循环引用和性能的更好处理,建议使用`structuredClone()`进行深层对象复制,而不是`JSON.stringify()`/`JSON.parse()`。 探索标签模板以进行高级字符串解析和操作。利用`WeakMap`/`WeakSet`将数据与对象关联,而不会阻止垃圾回收。新的集合操作,如`union`、`intersection`、`difference`和`isSubsetOf`,提供了强大的集合操作功能。

Hacker News 上的一个帖子讨论了一篇关于 2025 年 JavaScript 核心功能的博客文章。评论者 bhaney 对作者关于迭代器辅助函数的性能说法提出了质疑,他发现 `arr.values().drop().take().filter().map().toArray()` 比传统的 `arr.slice().filter().map()` 慢得多,而且内存分配也差不多。 Gnabgib 进一步解释说,迭代器辅助函数会多次分配内存(.values 和 .toArray),降低了效率。他们还批评了使用数组交换技巧来提高性能的建议。 Golergka 认为,大多数 JS/TS 代码优先考虑可读性而不是性能,但是关键的性能部分需要不同的编码风格。 Gnabgib 回复说,他们不同意新方法更易读,并继续指出 C# Linq 的性能损失几乎可以忽略不计,因为 C# 引擎进行了优化。但 JavaScript 还达不到这个水平,多个 JavaScript 引擎都需要赶上来。此外,迭代器辅助函数链中的单个函数无法预测后续步骤以提高效率,而优秀的软件工程师可以做到这一点。

原文

Published on

Problem description

JavaScript is constantly evolving and newer features are introduced. This oftentimes makes older coding practices outdated, and even less efficient. Bellow is a list of some important features (old and new) that most developers might be unaware of.

Iterator helpers

Have you ever performed multiple chained array transformations on the same array? For example, something like this: arr.slice(10, 20).filter(el => el < 10).map(el => el + 5). This is really inefficient because for each transformation a new array should be allocated. Imagine you do this for huge arrays (> 500K elements)... This is why JS has recently introduced methods for iterators. They work similar to regular array transformation methods, except they don't create temporary arrays but create new iterators which iterate on other iterators. This is the list of the methods:

  1. Iterator.prototype.drop(). Returns a new iterator helper object that skips the given number of elements at the start of this iterator. It roughly does the same thing as Array.prototype.slice(n) for the regular array.
  2. Iterator.prototype.take(). Returns a new iterator helper object that takes at most the given number of elements from the start of this iterator. It roughly does the same thing as Array.prototype.slice(0, n) for the regular array.
  3. Iterator.prototype.some(). Is similar to Array.prototype.some(). It tests whether at least one element produced by the iterator passes the test implemented by the provided function. element.
  4. Iterator.prototype.every(). Is similar to Array.prototype.every(). It tests whether all elements produced by the iterator pass the test implemented by the provided function.
  5. Iterator.prototype.filter(). Is similar to Array.prototype.filter(). Returns an iterator on the filtered values.
  6. Iterator.prototype.find(). Is similar to Array.prototype.find(). Returns the first element produced by the iterator that satisfies the provided testing function.
  7. Iterator.prototype.flatMap(). Is similar to Array.prototype.flatMap(). Returns an iterator on the flattened values.
  8. Iterator.prototype.forEach(). Is similar to Array.prototype.forEach(). It executes a provided function once for each element produced by the iterator.
  9. Iterator.prototype.map(). Is similar to Array.prototype.map(). Returns an iterator of transformed values by a mapping function.
  10. Iterator.prototype.reduce(). Is similar to Array.prototype.reduce(). It executes a user-supplied "reducer" callback function on each element produced by the iterator, passing in the return value from the calculation on the preceding element.
  11. Iterator.prototype.toArray(). Creates an Array with the populated yielded values.

Common ways for creating iterables are via the static method Iterator.from() and via the method values() of Array, NodeList, Set and many other containers.

So the more memory efficient version of the given example of transformation chaining will be: arr.values().drop(10).take(10).filter(el => el < 10).map(el => el + 5).toArray().

One caveat is that it's a relatively new feature, the last mainstream browser that started to support this feature is Safari. It started to support from , so better to wait at least for several months.

Array at() method

Array.prototype.at() is an alternative way for accessing the nth element. The cool thing is that it also supports negative indexing where it will count from the last element. For example [10,20,30].at(-1) will return 30, [10,20,30].at(-2) will return 20, etc. This negative indexing makes so much easier to access the last elements. Before that you had to write this ugly boilerplate code arr[arr.length - 1].

Promise.withResolvers()

Have you ever written such code in order to have the promise resolvers for the later use?

let resolve, reject; const promise = new Promise((resolver, rejector) => { resolve = resolver; reject = rejector; }); // use promise, resolve and reject later // ......

Bulky, isn't it? Fortunately, this is now a thing of past since JS nowadays supports Promise.withResolvers(). So you can write this instead:

const { promise, resolve, reject } = Promise.withResolvers(); // use promise, resolve and reject later // ......

String.prototype.replace() / String.prototype.replaceAll() callback

This is an old thing, but many developers don't know that you can pass a callback instead of string for the second parameter of String.prototype.replace() or String.prototype.replaceAll(). For example:

let counter = 0; console.log("NUMBER, NUMBER, NUMBER".replaceAll("NUMBER", (match) => match + "=" + (++counter))) // NUMBER=1, NUMBER=2, NUMBER=3

This is a very powerful thing, it allows to do many replacements with just one pass. Very efficient from both performance and memory standpoint. For more details please check the documentation.

Swapping variables

Another old thing. People oftentimes swap variables like this:

let a = 1, b = 2; console.log(a, b); // 1, 2 const temp = a; a = b; b = temp; console.log(a, b); // 2, 1

Do this instead:

let a = 1, b = 2; console.log(a, b); // 1, 2 [a, b] = [b, a]; console.log(a, b); // 2, 1

structuredClone()

Browsers nowadays support structuredClone() API. This is a very convenient function for deeply copying most regular objects. However, people oftentimes use JSON.stringify() and JSON.parse() for deeply copying objects without thinking whether it's appropriate or not.

This is where things can go wrong when doing JSON.stringify() / JSON.parse():

  1. JSON.stringify() doesn't support some values, such as NaN or undefined. They can be skipped or converted into null. For some data types, such as bigint, it will even throw exception.
  2. JSON.stringify() cannot work with objects that contain cyclic references: const obj = {}; obj.selfReference = obj; console.log(JSON.stringify(obj)); // exception
  3. Although usually not as serious as the first 2, but I must say it's not efficient for larger objects. It's slow and wastes a lot of memory.

structuredClone() should be preferred as much as possible. structuredClone() also automatically handles self referencing / cyclic structures.

const obj = {}; obj.selReference = obj; const clonedObj = structuredClone(obj); console.log(obj === clonedObj); // false, because it's a cloned object with a different memory address console.log(clonedObj.selReference === clonedObj); // true, because it has the same structure as obj (isomorphic to obj, i.e. as a graph)

For more advanced information about object cloning and comparison, please read this.

Tagged templates

Most of us are familiar with template literals (``), but many people are unaware of tagged templates. Tagged templates allow you to parse template literals with a function. The first argument of a tag function contains an array of string values. The remaining arguments are related to the expressions. Tagged templates are useful when you want to do some automatic transformations to the interpolated values (or even the whole string) (values inside `${... here ...}`).

For example we want to automatically escape html text when doing interpolation:

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> &lt;br&gt;

WeakMap / WeakSet

Besides Map and Set JavaScript also supports WeakMap and WeakSet. WeakMap and WeakSet are similar to Map and Set except that they don't allow primitive values for their keys and they lack iterators. This is done because when you lose all the references pointing to a key the key and, possibly, the associated value must have the possibility to be freed from the map / set and be garbage collected.

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 } // here we lost the references to key1 and key2, so the keys and values will be garbage collected later

Use WeakMap or WeakSet if you want to associate something with an object without any side effects.

Set operations

Recently JavaScript has added support for boolean operations with Set objects. Here is the list of boolean operations:

  1. Set.prototype.difference(). Returns a new set containing elements in this set but not in the given set. 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}
  2. Set.prototype.intersection(). Returns a new set containing elements in both this set and the given set. 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}
  3. Set.prototype.union(). Returns a new set containing elements which are in either or both of this set and the given set. 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}
  4. Set.prototype.symmetricDifference(). Returns a new set containing elements which are in either or both of this set and the given set. 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}
  5. Set.prototype.isDisjointFrom(). Returns a boolean indicating if this set has no elements in common with the given set. 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
  6. Set.prototype.isSubsetOf(). Returns a boolean indicating if all elements of this set are in the given set. 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
  7. Set.prototype.isSupersetOf(). Returns a boolean indicating if all elements of the given set are in this set. 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
联系我们 contact @ memedata.com