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 for
…of
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:
- Object spread vs.
Object.assign
- Converting an object to
Map
and vice-versa - The
for
…of
loop: should there remain only one… (this post) - Simulating an abstract class with
new.target
- Negative array indices thanks to proxies
Our story so far…
Historically, JavaScript had 4 loops:
- 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) {
// …
}
- The
for
…in
, 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])
}
- 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) {
// …
}
- The
do
…while
, 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 for
…in
, all these loops are found across a wide variety of programming languages.
What does for
…of
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, Map
s, Set
s, NodeList
s… Not only that, but many objects offer multiple iterators beyond their default iterability.
The for
…of
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 for
…of
? 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 for
…of
, 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 for
…of
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 for
…of
.
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).