Iterators & Generators

Why can for…of loop over arrays, strings, Maps and Sets, but not plain objects? Because those types follow the iteration protocol. This lesson shows how that protocol works, how to make your own objects iterable, and how generator functions make producing sequences of values easy.

The iteration protocol

An iterable is any object with a [Symbol.iterator]() method. That method returns an iterator: an object with a next() method that returns { value, done } each time it's called:

JS protocol.js
const letters = ["a", "b"];
const it = letters[Symbol.iterator]();

console.log(it.next());
console.log(it.next());
console.log(it.next());
Output
{ value: 'a', done: false }
{ value: 'b', done: false }
{ value: undefined, done: true }

for…of, spread syntax ([...x]), destructuring, Array.from, new Set(x) and Promise.all all use this protocol behind the scenes.

Making an object iterable by hand

JS manual.js
const range = {
  from: 1,
  to: 4,
  [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]);
for (const n of range) console.log(n * 10);
Output
[ 1, 2, 3, 4 ]
10
20
30
40

It works, but that's a lot of ceremony. Generators make it much simpler.

Generator functions

A generator function is written with function*. Calling it doesn't run the body — it returns an iterator. Each yield produces a value and pauses the function until the next value is requested:

JS generator.js
function* countdown(from) {
  console.log("starting");
  for (let i = from; i > 0; i--) {
    yield i;
  }
  console.log("liftoff");
}

const it = countdown(3);
console.log(it.next().value);
console.log(it.next().value);
console.log([...it]);
Output
starting
3
2
liftoff
[ 1 ]

Notice how the function pauses: "starting" prints only when the first value is requested, and "liftoff" only when the last value has been consumed.

Here's the range object again, with a generator as its iterator method:

JS range-generator.js
const range = {
  from: 1,
  to: 4,
  *[Symbol.iterator]() {
    for (let n = this.from; n <= this.to; n++) yield n;
  },
};

console.log([...range]);
Output
[ 1, 2, 3, 4 ]

Lazy and infinite sequences

Because generators only compute values on demand, they can describe sequences that never end — you just stop asking:

JS infinite.js
function* fibonacci() {
  let [a, b] = [0, 1];
  while (true) {
    yield a;
    [a, b] = [b, a + b];
  }
}

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

console.log([...take(fibonacci(), 10)].join(", "));
Output
0, 1, 1, 2, 3, 5, 8, 13, 21, 34

Iterator helpers

Modern JavaScript adds array-like methods directly to iterators — map, filter, take, drop, toArray and more. They're lazy, so they work on infinite sequences:

JS helpers.js
function* naturals() {
  let n = 1;
  while (true) yield n++;
}

const firstEvenSquares = naturals()
  .filter(n => n % 2 === 0)
  .map(n => n * n)
  .take(5)
  .toArray();

console.log(firstEvenSquares);
Output
[ 4, 16, 36, 64, 100 ]

Iterator helpers are supported in current Chrome, Edge, Firefox, Safari and Node.js.

yield* — delegating

JS delegate.js
function* inner() {
  yield "b";
  yield "c";
}

function* outer() {
  yield "a";
  yield* inner();
  yield* ["d", "e"];
}

console.log([...outer()].join(""));
Output
abcde

yield* yields every value from another iterable. This makes generators great for walking tree structures, such as nested menus or file systems.

Where you'll meet these

  • map.entries(), map.keys(), array.entries() and string.matchAll() all return iterators.
  • DOM collections like NodeList are iterable.
  • Async generators (async function* with for await…of) stream data such as paginated API results (lesson 53).

Try it yourself

Try it yourself
Console output