Iterators and generators

JavaScript · Advanced · 15 min

Iteration underlies for...of, spread and destructuring, and generators make lazy and infinite sequences easy.

An object is iterable if it has a [Symbol.iterator]() method returning an iterator: an object with a next() method that returns { value, done }. Arrays, strings, Maps, Sets, NodeLists and arguments are iterable, and everything that consumes iterables — for...of, spread, array destructuring, Array.from, new Map(...), Promise.all — works with yours too.

Writing next() by hand is tedious. A generator function (function*) does it for you. Calling it returns a generator object without running the body; each next() resumes execution until the next yield, which produces a value and pauses:

function* range(start, end, step = 1) {
  for (let i = start; i < end; i += step) yield i;
}
[...range(0, 10, 3)]   // [0, 3, 6, 9]

Generators keep their local state between calls, so they are naturally lazy: values are computed only when asked for. That makes infinite sequences possible, as long as the consumer stops. yield* other delegates to another iterable, handy for recursive structures like trees. A return inside ends iteration (done: true), and breaking out of for...of calls the generator's return() so its finally blocks run.

ES2025 adds iterator helpers on Iterator.prototype: map, filter, take, drop, flatMap, reduce, some, every, find, forEach and toArray. Unlike array methods they are lazy and work on any iterator, including infinite ones. Iterator.from(obj) wraps any iterable or iterator so it gets these methods.

Make a class iterable by defining *[Symbol.iterator]() { ... } as a generator method.

Example

function* naturals() {
  let n = 1;
  while (true) yield n++;          // infinite, but lazy
}

const firstOddSquares = naturals()
  .filter(n => n % 2)              // ES2025 iterator helpers
  .map(n => n * n)
  .take(4)
  .toArray();
console.log(firstOddSquares);      // [ 1, 9, 25, 49 ]

class Tree {
  constructor(value, children = []) { this.value = value; this.children = children; }
  *[Symbol.iterator]() {           // depth-first traversal
    yield this.value;
    for (const child of this.children) yield* child;
  }
}
const tree = new Tree("root", [new Tree("a", [new Tree("a1")]), new Tree("b")]);
console.log([...tree]);            // [ 'root', 'a', 'a1', 'b' ]

const it = [10, 20][Symbol.iterator]();
console.log(it.next(), it.next(), it.next());
// { value: 10, done: false } { value: 20, done: false } { value: undefined, done: true }

Remember

  • Iterable = has [Symbol.iterator](); iterator = has next() returning { value, done }
  • Generators pause at yield and are lazy — infinite sequences are fine
  • ES2025 iterator helpers (map, filter, take, toArray...) chain lazily

Watch out for

  • Spreading an infinite generator ([...naturals()]) — it never ends.

Check yourself

  1. When does the body of a generator function start executing?
  2. Why can naturals().map(f).take(3).toArray() finish even though naturals() is infinite?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_generators