All topics
ES6+advanced

Iterators and the Iterable Protocol

The formal protocol that makes an object usable with for...of, spread syntax, and destructuring, and how to implement it on custom objects.

An object is iterable if it has a Symbol.iterator method returning an iterator. That is the whole contract, and it is what for...of and spread rely on.

The iterable protocol is like a standardized vending machine interface: any manufacturer's machine implementing the same coin slot and dispensing mechanism can be used by the exact same universal vending customer without needing to know anything special about which brand it is.

Key Concepts

1
    const range = {
      from: 1, to: 5,
      [Symbol.iterator]() {
        let n = this.from, last = this.to;
        return {
          next: () => n <= last
            ? { value: n++, done: false }
            : { value: undefined, done: true }
        };
      }
    };
2
    [...range];                    // [1, 2, 3, 4, 5]
    for (const n of range) { }
3
An iterator is an object with next(), returning { value, done }. That is all.
next(){ value, done }
4
What the protocol unlocks, all at once.
    for...of
    spread             [...obj]
    destructuring      const [a, b] = obj
    Array.from(obj)
    new Set(obj), new Map(obj)
    Promise.all(obj)
    yield* obj
5
Implement one method and every one of those works.
6
Which is why built-ins are iterable. Arrays, strings, Map, Set, NodeList and arguments all define Symbol.iterator. Plain objects do not — which is why for...of over an object throws, and Object.entries exists.
MapSetNodeListargumentsSymbol.iterator
7
Generators implement the protocol for you.
    const range = {
      from: 1, to: 5,
      *[Symbol.iterator]() {
        for (let n = this.from; n <= this.to; n++) yield n;
      }
    };
8
Same behaviour, and the state management disappears. This is how the protocol is implemented in practice.
9
Laziness is the property that matters. An iterator produces values on demand, so an infinite sequence is fine as long as the consumer stops:
10
    function* naturals() { let n = 0; while (true) yield n++; }
11
    for (const n of naturals()) {
      if (n > 5) break;      // the generator is simply abandoned
    }
12
return() handles early exit. Breaking out of a for...of calls the iterator's return method if it has one, which is where a file handle or a connection gets closed — and why a generator's finally block runs on break.
return()for...ofreturnfinallybreak
13
Array-like is not iterable. An object with length and indices has neither Symbol.iterator nor array methods. Array.from handles both cases, which is why it is the general conversion.
lengthSymbol.iteratorArray.from
14
What the interviewer is probing.1. "What makes an object iterable?" Probing: the protocol. Stalls: "An array." Moves up: a Symbol.iterator method returning an iterator — an object with next() giving { value, done }.
15
2. "Which syntax consumes iterables?" Probing: the reach. Stalls: "Only for-of." Moves up: for-of, spread, destructuring, Array.from, Promise.all and the Map and Set constructors.
16
3. "Why is an object not iterable by default?" Probing: the design. Stalls: "It is." *Moves up:* property order and inheritance make it ambiguous, so you iterate Object.entries or define Symbol.iterator yourself.
17
4. "What is the easiest way to implement one?" Probing: generators. Stalls: "Write next by hand." Moves up: a generator function assigned to Symbol.iterator, which handles the state machine for you.