HomepageExploring JavaScript (ES2025 Edition)
You can support this book: buy it or donate
(Ad, please don’t block.)

32 Synchronous iteration ES6

32.1 What is synchronous iteration about?

Synchronous iteration is a protocol (interfaces plus rules for using them) that connects two groups of entities in JavaScript:

The iteration protocol connects these two groups via the interface Iterable: data sources deliver their contents sequentially “through it”; data consumers get their input from it.

Figure 32.1: Data consumers such as the for-of loop use the interface Iterable. Data sources such as Arrays implement that interface.

Figure 32.1 illustrates how iteration works: data consumers use the interface Iterable; data sources implement it.

Icon “details”The JavaScript way of implementing interfaces

In JavaScript, an object implements an interface if it has all the methods that it describes. The interfaces mentioned in this chapter only exist in the ECMAScript specification.

Both sources and consumers of data profit from this arrangement:

32.2 Core iteration roles: iterables and iterators

Two roles (described by interfaces) form the core of iteration (figure 32.2):

Figure 32.2: Iteration has two main interfaces: Iterable and Iterator. The former has a method that returns the latter.

These are type definitions (in TypeScript’s notation) for the interfaces of the iteration protocol:

interface Iterable<T> {
  [Symbol.iterator]() : Iterator<T>;
}

abstract class Iterator<T> {
  abstract next() : IteratorResult<T>;
}

interface IteratorResult<T> {
  value: T;
  done: boolean;
}

The interfaces are used as follows:

32.3 Iterating over data

32.3.1 Manually iterating over data

This is an example of using the iteration protocol:

const iterable = ['a', 'b'];

// The iterable is a factory for iterators:
const iterator = iterable[Symbol.iterator]();

// Call .next() until .done is true:
assert.deepEqual(
  iterator.next(), { value: 'a', done: false }
);
assert.deepEqual(
  iterator.next(), { value: 'b', done: false }
);
assert.deepEqual(
  iterator.next(), { value: undefined, done: true }
);

32.3.2 Using while to iterate manually

The following code demonstrates how to use a while loop to iterate over an iterable:

function logAll(iterable) {
  const iterator = iterable[Symbol.iterator]();
  while (true) {
    const {value, done} = iterator.next();
    if (done) break;
    console.log(value);
  }
}
logAll(['a', 'b']);

Output:

a
b

Icon “exercise”Exercise: Using sync iteration manually

exercises/sync-iteration/sync_iteration_manually_exrc.mjs

32.3.3 Retrieving an iterator via Iterator.from() ES2024

The built-in static method Iterator.from() provides us with a more elegant way of retrieving iterators:

> const iterable = ['a', 'b'];
> iterable[Symbol.iterator]() instanceof Iterator
true
> Iterator.from(iterable) instanceof Iterator
true

32.3.4 Iterating via iteration-based language constructs

We have seen how to use the iteration protocol manually and it is relatively cumbersome. But the protocol is not meant to be used directly – it is meant to be used via higher-level language constructs built on top of it. We’ll notice that we never see iterators when we do so. They are only used internally.

32.3.4.1 Iterating over Arrays

The most important iteration-based language construct is the for-of loop:

const iterable = ['hello', 'beautiful', 'world'];
for (const x of iterable) {
  console.log(x);
}

Output:

hello
beautiful
world

Another iteration-based construct is spreading into Array literals:

assert.deepEqual(
  ['BEFORE', ...iterable, 'AFTER'],
  ['BEFORE', 'hello', 'beautiful', 'world', 'AFTER']
);

Destructuring via Array patterns also uses iteration under the hood:

const [first, second] = iterable;
assert.equal(first, 'hello');
assert.equal(second, 'beautiful');
32.3.4.2 Iterating over Sets

Sets are also iterable. Note that the iterating code is the same: It sees neither Arrays nor Sets, only iterables.

const iterable = ['hello', 'beautiful', 'world'];

for (const x of iterable) {
  console.log(x);
}

assert.deepEqual(
  ['BEFORE', ...iterable, 'AFTER'],
  ['BEFORE', 'hello', 'beautiful', 'world', 'AFTER']
);

const [first, second] = iterable;
assert.equal(first, 'hello');
assert.equal(second, 'beautiful');

32.3.5 Converting iterables to Arrays: [...i] and Array.from(i)

These are ways of converting iterables to Arrays:

const iterable = new Set().add('a').add('b').add('c');
assert.deepEqual(
  [...iterable],
  ['a', 'b', 'c']
);
assert.deepEqual(
  Array.from(iterable),
  ['a', 'b', 'c']
);

I tend to prefer Array.from() because it’s more self-descriptive.

More information: “Converting iterables, iterators and Array-like values to Arrays” (§34.6)

We can also create an iterator and use an iterator method to create an Array. Iterator methods are explained later.

assert.deepEqual(
  Iterator.from(iterable).toArray(),
  ['a', 'b', 'c']
);

32.4 Processing iterables via generators

Synchronous generator functions and methods expose their yielded values via iterators (that are also iterable) that they return:

/** Synchronous generator function */
function* createSyncIterable() {
  yield 'a';
  yield 'b';
  yield 'c';
}

Generators produce iterables, but they can also consume them. That makes them a versatile tool for transforming iterables:

function* map(iterable, callback) {
  for (const x of iterable) {
    yield callback(x);
  }
}
assert.deepEqual(
  Array.from(
    map([1, 2, 3, 4], x => x ** 2)
  ),
  [1, 4, 9, 16]
);

function* filter(iterable, callback) {
  for (const x of iterable) {
    if (callback(x)) {
      yield x;
    }
  }
}
assert.deepEqual(
  Array.from(
    filter([1, 2, 3, 4], x => (x % 2) === 0
  )),
  [2, 4]
);

More information: “Synchronous generators ES6 (advanced)” (§33)

32.5 The inheritance of the iteration API (advanced)

All of the iterators created by JavaScript’s standard library have a common prototype which the ECMAScript specification calls %IteratorPrototype% and uses internally. We can access it from JavaScript via Iterator.prototype.

32.5.1 Array iterators

We create an Array iterator like this:

const arrayIterator = [][Symbol.iterator]();

This object has a prototype with two properties. Let’s call it ArrayIteratorPrototype:

const ArrayIteratorPrototype = Object.getPrototypeOf(arrayIterator);
assert.deepEqual(
  Reflect.ownKeys(ArrayIteratorPrototype),
  [ 'next', Symbol.toStringTag ]
);
assert.equal(
  ArrayIteratorPrototype[Symbol.toStringTag],
  'Array Iterator'
);

The prototype of ArrayIteratorPrototype is %IteratorPrototype%. This object has a method whose key is Symbol.iterator. Therefore, all built-in iterators are iterable.

const IteratorPrototype = Object.getPrototypeOf(ArrayIteratorPrototype);
assert.equal(
  IteratorPrototype, Iterator.prototype
);
assert.equal(
  Object.hasOwn(Iterator.prototype, Symbol.iterator),
  true
);
assert.equal(
  typeof Iterator.prototype[Symbol.iterator],
  'function'
);

The prototype of Iterator.prototype is Object.prototype.

assert.equal(
  Object.getPrototypeOf(Iterator.prototype) === Object.prototype,
  true
);

Figure 32.3 contains a diagram for this chain of prototypes.

Figure 32.3: A chain of prototypes (from bottom to top):

  • First the result of [][Symbol.iterator]() (an instance of %ArrayIterator%)
  • Then %ArrayIteratorPrototype%
  • Then %IteratorPrototype%
  • Finally Object.prototype

32.5.2 Generator objects

Roughly, a generator object is an iterator for the values yielded by a generator function genFunc(). We create it by calling genFunc():

function* genFunc() {
  yield 'a';
  yield 'b';
}
const genObj = genFunc();

A generator object is an iterator:

assert.deepEqual(
  genObj.next(),
  { value: 'a', done: false }
);
assert.equal(
  genObj instanceof Iterator,
  true
);
assert.equal(
  Iterator.prototype.isPrototypeOf(genObj),
  true
);

32.6 Iterable iterators

32.6.1 Why are the built-in iterators iterable?

As we have seen, all built-in iterators are iterable:

// Array iterator
const arrayIterator = ['a', 'b'].values();
assert.equal(
  // arrayIterator is a built-in iterator
  arrayIterator instanceof Iterator, true
);
assert.equal(
  // arrayIterator is iterable
  Symbol.iterator in arrayIterator, true
);

// Generator object
function* gen() { yield 'hello' }
const genObj = gen();
assert.equal(
  genObj instanceof Iterator, true
);
assert.equal(
  Symbol.iterator in genObj, true
);

// Iterator returned by `Iterator` method
const iter = Iterator.from([1, 2]).map(x => x * 2);
assert.equal(
  iter instanceof Iterator, true
);
assert.equal(
  Symbol.iterator in iter, true
);

That has the benefit of us being able to iterate over the iterator’s values – e.g., via for-of and Array.from().

Another benefit is that generators become more versatile. On one hand, we can use them to implement iterators:

class MyIterable {
  /** This method must return an iterator */
  * [Symbol.iterator]() {
    yield 'good';
    yield 'morning';
  }
}
assert.deepEqual(
  Array.from(new MyIterable()),
  ['good', 'morning']
);

On the other hand, we can use them to implement iterables:

function* createIterable() {
  yield 'a';
  yield 'b';
}
assert.deepEqual(
  Array.from(createIterable()),
  ['good', 'morning']
);

32.6.2 An iterator returns itself when asked for an iterator

If an iterator is iterable: What are the iterators it produces? It simply returns itself when asked for an iterator:

const iterator = Iterator.from(['a', 'b'])
assert.equal(
  iterator[Symbol.iterator](),
  iterator
);

32.6.3 Iteration quirk: two kinds of iterables

Alas, iterable iterators mean that there are two kinds of iterables:

  1. An iterable iterator is a one-time iterable: It always returns the same iterator (itself) when [Symbol.iterator]() is called (iteration continues).

  2. A normal iterable (an Array, a Set, etc.) is a many-times iterable: It always returns a fresh iterator (iteration restarts).

With a one-time iterable, each time we iterate, we remove more elements, until, eventually, no more are left:

const oneTime = ['a', 'b', 'c'].values();
for (const x of oneTime) {
  assert.equal(
    x, 'a'
  );
  break;
}
assert.deepEqual(
  Array.from(oneTime),
  ['b', 'c']
);
assert.deepEqual(
  Array.from(oneTime),
  []
);

With a many-times iterable, each iteration starts fresh:

const manyTimes = ['a', 'b', 'c'];
for (const x of manyTimes) {
  assert.equal(
    x, 'a'
  );
  break;
}
assert.deepEqual(
  Array.from(manyTimes),
  ['a', 'b', 'c']
);
assert.deepEqual(
  Array.from(manyTimes),
  ['a', 'b', 'c']
);

The following code is another demonstration of the difference:

const oneTime = ['a', 'b', 'c'].values();
assert.deepEqual(
  [...oneTime, ...oneTime, ...oneTime],
  ['a', 'b', 'c']
);

const manyTimes = ['a', 'b', 'c'];
assert.deepEqual(
  [...manyTimes, ...manyTimes, ...manyTimes],
  ['a','b','c', 'a','b','c', 'a','b','c']
);

32.7 Class Iterator and iterator helper methods ES2025

We have already seen that %IteratorPrototype% is the prototype of all built-in iterators. ECMAScript 2025 introduces a class Iterator:

The class provides the following functionality:

32.7.1 Iterator.prototype.* methods

The following iterator helper methods work like the Array methods with the same names:

These helper methods are unique to iterators:

For a brief description of each method, see “Quick reference: class Iterator ES2025” (§32.10). These are examples of the methods in action:

assert.deepEqual(
  ['a', 'b', 'c'].values().map(x => `=${x}=`).toArray(),
  ['=a=', '=b=', '=c=']
);
assert.deepEqual(
  ['a', 'b', 'c'].values().drop(1).toArray(),
  ['b', 'c']
);
assert.deepEqual(
  ['a', 'b', 'c'].values().take(2).toArray(),
  ['a', 'b']
);

The Array method arr.values() returns an iterator over the elements of arr.

Icon “exercise”Exercises: Working with iterators

32.7.2 The benefits of iterator helper methods

32.7.2.1 Benefit: more operations for data structures that support iteration

With iterator helper methods, any data structure that supports iteration automatically gains functionality.

For example, Sets don’t support the operations filter and map, but we can get them via iterator methods:

assert.deepEqual(
  new Set( // (A)
    new Set([-5, 2, 6, -3]).values().filter(x => x >= 0)
  ),
  new Set([2, 6])
);
assert.deepEqual(
  new Set( // (B)
    new Set([-5, 2, 6, -3]).values().map(x => x / 2)
  ),
  new Set([-2.5, 1, 3, -1.5])
);

Note that new Set() accepts iterables and therefore iterable iterators (line A and line B).

DOM collections also don’t have the methods .filter() and .map():

const domCollection = document.querySelectorAll('a');

// Alas, the collection doesn’t have a method .map()
assert.equal('map' in domCollection, false);

// Solution: use an iterator
assert.deepEqual(
  domCollection.values().map(x => x.href).toArray(),
  ['https://2ality.com', 'https://exploringjs.com']
);

Icon “exercise”Exercise: Implementing .filter() and .map() for Sets via iterator methods

exercises/sync-iteration/set-operations-via-iterators_test.mjs

32.7.2.2 Benefit: no intermediate Arrays and incremental processing

If we chain operations that return Arrays (line A, line B, line C) then each operation produces a new Array:

function quoteNonEmptyLinesArray(str) {
  return str
    .split(/(?<=\r?\n)/) // (A)
    .filter(line => line.trim().length > 0) // (B)
    .map(line => '> ' + line) // (C)
    ;
}

The regular expression in line A contains a lookbehind assertion which ensures that the lines returned by .split() includes line terminators.

In contrast, each operation (line A, line B, line C) in the following code returns an iterator and no intermediate Arrays are created:

function quoteNonEmptyLinesIter(str) {
  return splitLinesIter(str) // (A)
    .filter(line => line.trim().length > 0) // (B)
    .map(line => '> ' + line) // (C)
    ;
}

function* splitLinesIter(str) {
  let prevIndex = 0;
  while (true) {
    const eolIndex = str.indexOf('\n', prevIndex);
    if (eolIndex < 0) break;
    // Including EOL
    const line = str.slice(prevIndex, eolIndex + 1);
    yield line;
    prevIndex = eolIndex + 1;
  }
  if (prevIndex < str.length) {
    yield str.slice(prevIndex);
  }
}

Example of using quoteNonEmptyLinesIter():

assert.deepEqual(
  Array.from(quoteNonEmptyLinesIter('have\n\na nice\n\nday')),
  [
    '> have\n',
    '> a nice\n',
    '> day',
  ]
);

Note that the empty lines between the three lines of text were filtered out.

In addition to no intermediate Arrays being created, iterators also give us incremental processing:

> const iter = quoteNonEmptyLinesIter('have\n\na nice\n\nday');
> iter.next()
{ value: '> have\n', done: false }
> iter.next()
{ value: '> a nice\n', done: false }
> iter.next()
{ value: '> day', done: false }
> iter.next()
{ value: undefined, done: true }

In contrast, quoteNonEmptyLinesArray() first splits all lines, then filters all lines and then maps all lines. Incremental processing matters when dealing with a large amount of data. Iterator helper methods complement generators as tools for incremental processing.

32.7.3 Iterator.from(): creating API iterators

All built-in iterables automatically support the new API because their iterators already have Iterator.prototype as a prototype (and are therefore instances of Iterator). However, that’s not the case for all iterables in libraries and user code.

Terminology:

How does Iterator.from(obj) work?

In the following example, we use Iterator.from() to convert a legacy iterator to an API iterator:

// Not an instance of `Iterator`
const legacyIterator = {
  next() {
    // Infinite iterator (never done)
    return { done: false, value: '#' };
  }
};
assert.equal(
  Iterator.from(legacyIterator) instanceof Iterator,
  true
);
assert.deepEqual(
  Iterator.from(legacyIterator).take(3).toArray(),
  ['#', '#', '#']
);

32.7.4 Iterator methods change how we use iteration

The iterator methods change how we use iteration:

It’s interesting how our focus shifts with methods such as Array.prototype.keys() that return iterable iterators: Before iterator methods, we used the result as an iterable. With iterator methods, we also use them as iterators:

> Array.from(['a', 'b'].keys()) // iterable
[ 0, 1 ]
> ['a', 'b'].keys().toArray() // iterator
[ 0, 1 ]

For more information, see “Creating iterators” (§32.10.1).

32.7.5 Upgrading a legacy iterable to the Iterator API

This is an example of a legacy iterable:

class ValueIterable {
  #values;
  #index = 0;
  constructor(...values) {
    this.#values = values;
  }
  [Symbol.iterator]() {
    return {
      // Arrow function so that we can use the outer `this`
      next: () => {
        if (this.#index >= this.#values.length) {
          return {done: true};
        }
        const value = this.#values[this.#index];
        this.#index++;
        return {done: false, value};
      },
    };
  }
}

// legacyIterable is an iterable
const legacyIterable = new ValueIterable('a', 'b', 'c');
assert.deepEqual(
  Array.from(new ValueIterable('a', 'b', 'c')),
  ['a', 'b', 'c']
);

// But its iterators are not instances of Iterator
const legacyIterator = legacyIterable[Symbol.iterator]();
assert.equal(
  legacyIterator instanceof Iterator, false
);

If we want ValueIterable to support the Iterator API, we have to make its iterators instances of Iterator:

class ValueIterable {
  // ···
  [Symbol.iterator]() {
    return {
      __proto__: Iterator.prototype,
      next: () => {
        // ···
      },
    };
  }
}

This is another option (albeit a less efficient one):

class ValueIterable {
  // ···
  [Symbol.iterator]() {
    return Iterator.from({
      next: () => {
        // ···
      },
    });
  }
}

We can also create a class for iterators:

class ValueIterable {
  #values;
  #index = 0;
  constructor(...values) {
    this.#values = values;
  }
  [Symbol.iterator]() {
    const outerThis = this;
    // Because ValueIterator is nested, it can access the private fields of
    // ValueIterable
    class ValueIterator extends Iterator {
      next() {
        if (outerThis.#index >= outerThis.#values.length) {
          return {done: true};
        }
        const value = outerThis.#values[outerThis.#index];
        outerThis.#index++;
        return {done: false, value};
      }
    }
    return new ValueIterator();
  }
}

32.8 Grouping iterables ES2024

Map.groupBy() groups the items of an iterable into Map entries whose keys are provided by a callback:

assert.deepEqual(
  Map.groupBy([0, -5, 3, -4, 8, 9], x => Math.sign(x)),
  new Map().set(0, [0]).set(-1, [-5,-4]).set(1, [3,8,9])
);

The items to be grouped can come from any iterable:

function* generateNumbers() {
  yield 2;
  yield -7;
  yield 4;
}
assert.deepEqual(
  Map.groupBy(generateNumbers(), x => Math.sign(x)),
  new Map().set(1, [2,4]).set(-1, [-7])
);

There is also Object.groupBy() which produces an object instead of a Map:

assert.deepEqual(
  Object.groupBy([0, -5, 3, -4, 8, 9], x => Math.sign(x)),
  {'0': [0], '-1': [-5,-4], '1': [3,8,9], __proto__: null}
);

32.8.1 Choosing between Map.groupBy() and Object.groupBy()

32.8.2 Example: handling cases

The Promise combinator Promise.allSettled() returns Arrays such as the following one:

const settled = [
  { status: 'rejected', reason: 'Jhon' },
  { status: 'fulfilled', value: 'Jane' },
  { status: 'fulfilled', value: 'John' },
  { status: 'rejected', reason: 'Jaen' },
  { status: 'rejected', reason: 'Jnoh' },
];

We can group the Array elements as follows:

const {fulfilled, rejected} = Object.groupBy(settled, x => x.status); // (A)

// Handle fulfilled results
assert.deepEqual(
  fulfilled,
  [
    { status: 'fulfilled', value: 'Jane' },
    { status: 'fulfilled', value: 'John' },
  ]
);

// Handle rejected results
assert.deepEqual(
  rejected,
  [
    { status: 'rejected', reason: 'Jhon' },
    { status: 'rejected', reason: 'Jaen' },
    { status: 'rejected', reason: 'Jnoh' },
  ]
);

For this use case, Object.groupBy() works better because we can use destructuring (line A).

32.8.3 Example: grouping by property value

In the next example, we’d like to group persons by country:

const persons = [
  { name: 'Louise', country: 'France' },
  { name: 'Felix', country: 'Germany' },
  { name: 'Ava', country: 'USA' },
  { name: 'Léo', country: 'France' },
  { name: 'Oliver', country: 'USA' },
  { name: 'Leni', country: 'Germany' },
];

assert.deepEqual(
  Map.groupBy(persons, (person) => person.country),
  new Map([
    [
      'France',
      [
        { name: 'Louise', country: 'France' },
        { name: 'Léo', country: 'France' },
      ]
    ],
    [
      'Germany',
      [
        { name: 'Felix', country: 'Germany' },
        { name: 'Leni', country: 'Germany' },
      ]
    ],
    [
      'USA',
      [
        { name: 'Ava', country: 'USA' },
        { name: 'Oliver', country: 'USA' },
      ]
    ],
  ])
);

For this use case, Map.groupBy() is a better choice because we can use arbitrary keys in Maps whereas in objects, keys are limited to strings and symbols.

Icon “exercise”Exercise: Using Map.groupBy() for an Array of objects

exercises/sync-iteration/count-cities_test.mjs

32.9 Quick reference: synchronous iteration

32.9.1 Synchronous iteration: data producers

These data structures are iterable:

The following data structures have the methods .keys(), .values(), and .entries() that return iterables that are not Arrays:

As an aside – the following static methods list property keys, values and entries (they are not normal methods because those can be accidentally overridden). They return Arrays.

Synchronous generator functions and methods expose their yielded values via iterable objects that they return:

/** Synchronous generator function */
function* createSyncIterable() {
  yield 'a';
  yield 'b';
  yield 'c';
}

assert.deepEqual(
  Array.from(createSyncIterable()),
  ['a', 'b', 'c']
);

32.9.2 Synchronous iteration: data consumers

This section lists constructs that consume data via synchronous iteration.

32.9.2.1 Language constructs that iterate
32.9.2.2 Converting iterables to data structures
32.9.2.3 Converting iterables over Promises to Promises
32.9.2.4 Grouping an iterable into a Map or an object

32.10 Quick reference: class Iterator ES2025

32.10.1 Creating iterators

The methods of class Iterator let us process data incrementally. Let’s explore where we can use them.

32.10.1.1 Getting iterators from iterables
32.10.1.2 Built-in methods that return iterators

Arrays, Typed Arrays, Sets and Maps have additional methods that return iterators:

The following methods return iterators:

32.10.1.3 Other sources of iterators

Generators also return iterators:

function* gen() {}

assert.equal(
  gen() instanceof Iterator,
  true
);

32.10.2 Iterator.*

32.10.3 Iterator.prototype.*: methods that pass indices to callbacks

Some of the iterator methods keep a counter for the iterated values and pass it on to their callbacks:

32.10.4 Iterator.prototype.*: methods that return iterators

32.10.5 Iterator.prototype.*: methods that return booleans

32.10.6 Iterator.prototype.*: methods that return other kinds of values

32.10.7 Iterator.prototype.*: other methods