Some features that every JavaScript developer should know in 2025

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 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
Previous