Please support this book: buy it or donate

27. Synchronous iteration



27.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:

  • Data sources: On one hand, data comes in all shapes and sizes. In JavaScript’s standard library, you have the linear data structure Array, the ordered collection Set (elements are ordered by time of addition), the ordered dictionary Map (entries are ordered by time of addition), and more. In libraries, you may find tree-shaped data structures and more.

  • Data consumers: On the other hand, you have a whole class of mechanisms and algorithms that only need to access values sequentially: one at a time plus a way to tell when everything is done. Examples include the for-of loop and spreading into Array literals (via ).

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

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

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

The diagram in fig. 17 illustrates how iteration works: data consumers use the interface Iterable; data sources implement it. Note that in JavaScript, implementing only means having the methods described by the interface; the interface itself only exists in the specification.

Both sources and consumers of data profit from this arrangement:

  • If you develop a new data structure, you only need to implement Iterable and a raft of tools can immediately be applied to it.

  • If you write code that uses iteration, it automatically works with many sources of data.

27.2. Core iteration constructs: iterables and iterators

Two roles (described by interfaces) form the core of iteration (fig. 18):

  • An iterable is an object whose contents can be traversed sequentially.
  • An iterator is the pointer used for the traversal.
    Figure 18: Iteration has two main interfaces: Iterable and Iterator. The former has a method that returns the latter.
    Figure 18: 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:

  1. interface Iterable<T> {
  2. [Symbol.iterator]() : Iterator<T>;
  3. }
  4. interface Iterator<T> {
  5. next() : IteratorResult<T>;
  6. }
  7. interface IteratorResult<T> {
  8. value: T;
  9. done: boolean;
  10. }

The interfaces are used as follows:

  • You ask an Iterable for an iterator via the method whose key is Symbol.iterator.
  • The Iterator returns the iterated values via its method .next().
  • The values are not returned directly, but wrapped in objects with two properties:
    • .value is the iterated value.
    • .done indicates if the end of the iteration has been reached, yet. It is true after the last iterated value and false beforehand.

27.3. Iterating manually

This is an example of using the iteration protocol:

  1. const iterable = ['a', 'b'];
  2. // The iterable is a factory for iterators:
  3. const iterator = iterable[Symbol.iterator]();
  4. // Call .next() until .done is true:
  5. assert.deepEqual(
  6. iterator.next(), { value: 'a', done: false });
  7. assert.deepEqual(
  8. iterator.next(), { value: 'b', done: false });
  9. assert.deepEqual(
  10. iterator.next(), { value: undefined, done: true });

27.3.1. Iterating over an iterable via while

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

  1. function logAll(iterable) {
  2. const iterator = iterable[Symbol.iterator]();
  3. while (true) {
  4. const {value, done} = iterator.next();
  5. if (done) break;
  6. console.log(value);
  7. }
  8. }
  9. logAll(['a', 'b']);
  10. // Output:
  11. // 'a'
  12. // 'b'

27.4. Iteration in practice

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. This section shows what that looks like.

27.4.1. Arrays

JavaScript’s Arrays are iterable. That enables you to use the for-of loop:

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

Destructuring via Array patterns (explained later) also uses iteration, under the hood:

  1. const [first, second] = myArray;
  2. assert.equal(first, 'a');
  3. assert.equal(second, 'b');

27.4.2. Sets

JavaScript’s Set data structure is iterable. That means, for-of works:

  1. const mySet = new Set().add('a').add('b').add('c');
  2. for (const x of mySet) {
  3. console.log(x);
  4. }
  5. // Output:
  6. // 'a'
  7. // 'b'
  8. // 'c'

As does Array-destructuring:

  1. const [first, second] = mySet;
  2. assert.equal(first, 'a');
  3. assert.equal(second, 'b');

27.5. Quick reference: synchronous iteration

27.5.1. Iterable data sources

The following built-in data sources are iterable:

  • Arrays
  • Strings
  • Maps
  • Sets
  • (Browsers: DOM data structures)
    To iterate over the properties of objects, you need helpers such as Object.keys() and Object.entries(). That is necessary, because properties exist at a different level that is complementary to the level of data structures.

27.5.2. Iterating constructs

The following constructs support iteration:

  • Destructuring via an Array pattern:
  1. const [x,y] = iterable;
  • The for-of loop:
  1. for (const x of iterable) { /*···*/ }
  • Array.from():
  1. const arr = Array.from(iterable);
  • Spreading (via ) into Arrays and function calls:
  1. const arr = [...iterable];
  2. func(...iterable);
  • new Map() and new Set():
  1. const m = new Map(iterableOverKeyValuePairs);
  2. const s = new Set(iterableOverElements);
  • Promise.all() and Promise.race():
  1. const promise1 = Promise.all(iterableOverPromises);
  2. const promise2 = Promise.race(iterableOverPromises);
  • yield*:
  1. function* generatorFunction() {
  2. yield* iterable;
  3. }

27.6. Further reading

For more details on synchronous iteration, consult “Exploring ES6”.