Kodokon kodokon.com

Iterators, generators, and iteration protocols

Master the protocols behind for...of, spread, and destructuring, then use generators to produce lazy sequences.

9 min · 3 questions

Open this lesson in Kodokon

Two distinct protocols govern iteration. An object is iterable if it exposes a method under the Symbol.iterator key; that method must return an iterator, that is, an object with a next() method returning { value, done }. Everything that consumes sequences - for...of, the spread operator ..., destructuring, Array.from, Promise.all - goes through these protocols. You can therefore make any object compatible.

JAVASCRIPT
const range = {
  from: 1,
  to: 3,
  [Symbol.iterator]() {
    let current = this.from;
    const last = this.to;
    return {
      next() {
        return current <= last
          ? { value: current++, done: false }
          : { value: undefined, done: true };
      }
    };
  }
};

console.log([...range]); // [1, 2, 3]
A hand-written iterable, with no class or generator.

Writing iterators by hand is verbose. Generators (function*) produce an iterator automatically: each yield suspends execution and preserves the function's local state. This lazy evaluation lets you model infinite sequences, as long as a consumer limits the number of values requested.

JAVASCRIPT
function* naturals() {
  let n = 0;
  while (true) yield n++;
}

function* take(iterable, count) {
  let index = 0;
  for (const value of iterable) {
    if (index >= count) return;
    yield value;
    index += 1;
  }
}

console.log([...take(naturals(), 4)]); // [0, 1, 2, 3]
An infinite sequence consumed without ever blocking.

Generator iterators also expose return() and throw(). A spec detail that often goes unnoticed: when you leave a for...of loop with break, return, or an exception, the engine automatically calls iterator.return(). In a generator, that runs any pending finally blocks - the ideal place to release a resource (a file, a connection, a lock).

JAVASCRIPT
function* readLines() {
  try {
    yield "line 1";
    yield "line 2";
  } finally {
    console.log("cleanup");
  }
}

for (const line of readLines()) {
  console.log(line);
  break; // triggers return(), hence the finally
}
// line 1, cleanup
break calls return(): the finally block runs.

Knowledge check

Make sure you remember the key points of this lesson.

  1. What makes an object iterable in the sense of the protocol?
    • Having a length property and numeric indexes
    • Exposing a Symbol.iterator method that returns an object with next()
    • Inheriting from Array.prototype
    • Having a next() method directly
  2. What happens when you exit a for...of loop with break while it is consuming a generator?
    • Nothing special, the generator stays suspended at the last yield
    • The engine calls throw() on the iterator
    • The engine calls return(), which runs the generator's finally blocks
    • The generator restarts from the beginning on the next iteration
  3. Why can you work with an infinite generator without blocking the program?
    • The engine automatically caps the number of iterations
    • Generators run on a separate thread
    • Evaluation is lazy: each value is only computed when next() is called
    • The values are precomputed and cached by the engine