Please support this book: buy it (PDF, EPUB, MOBI) or donate

17. The for-of loop

17.1 Overview

for-of is a new loop in ES6 that replaces both for-in and forEach() and supports the new iteration protocol.

Use it to loop over iterable objects (Arrays, strings, Maps, Sets, etc.; see Chap. “Iterables and iterators”):

  1. const iterable = ['a', 'b'];
  2. for (const x of iterable) {
  3. console.log(x);
  4. }
  5.  
  6. // Output:
  7. // a
  8. // b

break and continue work inside for-of loops:

  1. for (const x of ['a', '', 'b']) {
  2. if (x.length === 0) break;
  3. console.log(x);
  4. }
  5.  
  6. // Output:
  7. // a

Access both elements and their indices while looping over an Array (the square brackets before of mean that we are using destructuring):

  1. const arr = ['a', 'b'];
  2. for (const [index, element] of arr.entries()) {
  3. console.log(`${index}. ${element}`);
  4. }
  5.  
  6. // Output:
  7. // 0. a
  8. // 1. b

Looping over the [key, value] entries in a Map (the square brackets before of mean that we are using destructuring):

  1. const map = new Map([
  2. [false, 'no'],
  3. [true, 'yes'],
  4. ]);
  5. for (const [key, value] of map) {
  6. console.log(`${key} => ${value}`);
  7. }
  8.  
  9. // Output:
  10. // false => no
  11. // true => yes

17.2 Introducing the for-of loop

for-of lets you loop over data structures that are iterable: Arrays, strings, Maps, Sets and others. How exactly iterability works is explained in Chap. “Iterables and iterators”. But you don’t have to know the details if you use the for-of loop:

  1. const iterable = ['a', 'b'];
  2. for (const x of iterable) {
  3. console.log(x);
  4. }
  5.  
  6. // Output:
  7. // a
  8. // b

for-of goes through the items of iterable and assigns them, one at a time, to the loop variable x, before it executes the body. The scope of x is the loop, it only exists inside it.

You can use break and continue:

  1. for (const x of ['a', '', 'b']) {
  2. if (x.length === 0) break;
  3. console.log(x);
  4. }
  5.  
  6. // Output:
  7. // a

for-of combines the advantages of:

  • Normal for loops: break/continue; usable in generators
  • forEach() methods: concise syntax

17.3 Pitfall: for-of only works with iterable values

The operand of the of clause must be iterable. That means that you need a helper function if you want to iterate over plain objects (see “Plain objects are not iterable”). If a value is Array-like, you can convert it to an Array via Array.from():

  1. // Array-like, but not iterable!
  2. const arrayLike = { length: 2, 0: 'a', 1: 'b' };
  3.  
  4. for (const x of arrayLike) { // TypeError
  5. console.log(x);
  6. }
  7.  
  8. for (const x of Array.from(arrayLike)) { // OK
  9. console.log(x);
  10. }

17.4 Iteration variables: const declarations versus var declarations

If you const-declare the iteration variable, a fresh binding (storage space) will be created for each iteration. That can be seen in the following code snippet where we save the current binding of elem for later, via an arrow function. Afterwards, you can see that the arrow functions don’t share the same binding for elem, they each have a different one.

  1. const arr = [];
  2. for (const elem of [0, 1, 2]) {
  3. arr.push(() => elem); // save `elem` for later
  4. }
  5. console.log(arr.map(f => f())); // [0, 1, 2]
  6.  
  7. // `elem` only exists inside the loop:
  8. console.log(elem); // ReferenceError: elem is not defined

A let declaration works the same way as a const declaration (but the bindings are mutable).

It is instructive to see how things are different if you var-declare the iteration variable. Now all arrow functions refer to the same binding of elem.

  1. const arr = [];
  2. for (var elem of [0, 1, 2]) {
  3. arr.push(() => elem);
  4. }
  5. console.log(arr.map(f => f())); // [2, 2, 2]
  6.  
  7. // `elem` exists in the surrounding function:
  8. console.log(elem); // 2

Having one binding per iteration is very helpful whenever you create functions via a loop (e.g. to add event listeners).

You also get per-iteration bindings in for loops (via let) and for-in loops (via const or let). Details are explained in the chapter on variables.

17.5 Iterating with existing variables, object properties and Array elements

So far, we have only seen for-of with a declared iteration variable. But there are several other forms.

You can iterate with an existing variable:

  1. let x;
  2. for (x of ['a', 'b']) {
  3. console.log(x);
  4. }

You can also iterate with an object property:

  1. const obj = {};
  2. for (obj.prop of ['a', 'b']) {
  3. console.log(obj.prop);
  4. }

And you can iterate with an Array element:

  1. const arr = [];
  2. for (arr[0] of ['a', 'b']) {
  3. console.log(arr[0]);
  4. }

17.6 Iterating with a destructuring pattern

Combining for-of with destructuring is especially useful for iterables over [key, value] pairs (encoded as Arrays). That’s what Maps are:

  1. const map = new Map().set(false, 'no').set(true, 'yes');
  2. for (const [k,v] of map) {
  3. console.log(`key = ${k}, value = ${v}`);
  4. }
  5. // Output:
  6. // key = false, value = no
  7. // key = true, value = yes

Array.prototype.entries() also returns an iterable over [key, value] pairs:

  1. const arr = ['a', 'b', 'c'];
  2. for (const [k,v] of arr.entries()) {
  3. console.log(`key = ${k}, value = ${v}`);
  4. }
  5. // Output:
  6. // key = 0, value = a
  7. // key = 1, value = b
  8. // key = 2, value = c

Therefore, entries() gives you a way to treat iterated items differently, depending on their position:

  1. /** Same as arr.join(', ') */
  2. function toString(arr) {
  3. let result = '';
  4. for (const [i,elem] of arr.entries()) {
  5. if (i > 0) {
  6. result += ', ';
  7. }
  8. result += String(elem);
  9. }
  10. return result;
  11. }

This function is used as follows:

  1. > toString(['eeny', 'meeny', 'miny', 'moe'])
  2. 'eeny, meeny, miny, moe'