The for…of loop: should there remain only one…
Published on 20 May 2020 • 4 min

Cette page est également disponible en français.

Welcome to the seventeenth installment in our daily series “19 nuggets of vanilla JS.” Today we talk about one of my favorite new (ES2015+) language features: the forof loop. Still vastly underused or even poorly known, it advantageously replaces most numerical for loops and forEach calls, and offers many side benefits.

The series of 19

Check out surrounding posts from the series:

  1. Object spread vs. Object.assign
  2. Converting an object to Map and vice-versa
  3. The forof loop: should there remain only one… (this post)
  4. Simulating an abstract class with new.target
  5. Negative array indices thanks to proxies

Our story so far…

Historically, JavaScript had 4 loops:

  1. The numerical for, coming over from C and widespread in other languages. Its syntax is completely obscure to beginners, but hey, it is what it is…
for (var index = 0, len = items.length; index < len; ++index) {
// …
}
  1. The forin, that is nothing like similar constructs in other languages. It is specifically designed to iterate over enumerable properties of an object, regardless of whether they’re inherited (through the prototype chain) or own (object-specific) properties.
var person = { first: 'John', last: 'Smith' }
for (var prop in person) {
// In sequence: 'first' and 'last' (unless a !@# polluted
// `Object.prototype`…)
console.log(prop, '=', person[prop])
}
  1. The while, again available across many, many languages. Instead of following a sequence, it relies on a condition evaluated at the beginning of every turn, which serves as a “keep going” indicator (so it is possible never to enter such a loop):
while (cond) {
// …
}
  1. The dowhile, which is similar but uses an end-of-turn condition, so you’ll run through the loop at least once.
do {
// …
} while (cond)

With the exception of forin, all these loops are found across a wide variety of programming languages.

What does forof do?

ES2015 formalizes the all-important notion of iterability. The iterable protocol is clearly defined (via the Symbol.iterator built-in — “well-known” — symbol), and many built-in objects are iterable: arrays obviously, but also strings, Maps, Sets, NodeLists… Not only that, but many objects offer multiple iterators beyond their default iterability.

The forof loop is the only way to consume iterables that provides full control to your code: you can consume just as much as you need, with the quantity itself being dynamically defined by your algorithm.

Say you want to consume a good ol’ array, focusing as usually on values, not indices; this becomes much more palatable:

// Meh.
for (var index = 0, len = items.length; index < len; ++index) {
var item = items[index]
// …
}

// Sweet!
for (const item of items) {
// …
}

As with any loop, you are free to leverage break, continue and return in there. It is, however, much more versatile than a numerical for: it works without indices / positions (e.g. on a Set), and you don’t need to remember caching the length for performance…

A nice opportunity to use const

Did you notice how we could prefer const when using forof? That’s because we don’t need to reassign anything: we work directly with the value, not with an index we need to manually move forward. So we may as well declare it as const to avoid mishaps.

On-the-fly destructuring

When the iterator you’re consuming emits value tuples, feel free to destructure on-the-fly. As an example, instead of doing this:

const map = new Map([
[1, 'one'],
[2, 'two'],
[3, 'three'],
])
// This could be better…
for (const pair of map) {
console.log(pair[0], '=', pair[1])
}

Feel free to do this:

// …like so:
for (const [key, value] of map) {
console.log(key, '=', value)
}

Keep an eye out for extra iterators

Many iterables offer additional iterators besides their default one. Most offer at least three conventional methods: keys(), values() and entries(). When the concept of a key is missing (as in Set), keys are… the values. You can also sometimes find more domain-specific iterators. Sky’s the limit! (Unless you’re working on, say, the tactile UI in SpaceX’s Crew Dragon. In which case, mad ~envy~respect.)

Say we need to consume an Array using forof, but need to get the indices too. This just needs the right iterator:

for (const [index, item] of items.entries()) {
// …
}

Friendly to lazy evaluation

A major benefit of forof is this: since it doesn’t necessarily consume the whole iterable, but rather on an as-you-go basis, it is the main way to consume lazy-evaluated computations, including infinite iterables such as mathematical sequences, some crypto stuff, value generators, etc.

Say we have a generator implementing the Fibonacci sequence:

function* fibonacci() {
let [current, next] = [1, 1]
while (true) {
yield current
;[current, next] = [next, current + next]
}
}

This sequence never ends: should we try to Array.from(…) or spread it, we’d run out of memory (although the JS runtime would likely give us the ax before that). We can however get the first few terms by, say, positional destructuring:

const [a, b, c, d, e] = fibonacci()
// a === 1, b === 1, c === 2, d === 3, e === 5

Yet how could we browse it as-we-go, without knowing ahead of time how many terms we want? We need a loop so we can exit on-demand. Should we need to display all terms below 100, we could go like this:

for (const term of fibonacci()) {
if (term > 100) break
console.log(term)
}

All lazy-evaluation primitives (a pervading concept in functional programming) can be implemented that way, for instance the ahead-of-time consumption capping with take:

function* take(count, iter) {
if (count === 0) return
for (const term of iter) {
yield term
if (--count <= 0) break
}
}

(If you use RxJS or Ramda, this should look familiar…)

Performance

You can read every possible opinion online in terms of performance benchmarks for competing loop styles. Most are not very relevant / useful. What you need to keep in mind are two things:

  • As long as you’re not iterating across huge arrays (on the order of 1M+ items), the difference will be negligible.
  • Even beyond that, if your loop is not transpiled (see further below about native support), these are pretty much equivalent.

You should therefore hardly ever need to go back to a numerical for or a while for these scenarios.

Bye old-timers!

This means you can replace the vast majority of your legacy iteration mechanisms (including the numerical for, forEach(…), or jQuery/Lodash’s each(…)) with a neat, clean and versatile forof.

Where can I get it?

It’s been natively supported since Firefox 13, Chrome 38, Opera 25, Edge 12, Safari 7 and Node 0.12.

As for older platforms, Babel and TypeScript will transpile it, but on huge arrays (1M+ items), performance could suffer a bit (even then, it all depends on your JS engine, your algorithm, and other pieces of context).