JavaScript Iterator Table
| Piece | Protocol role | Field note |
|---|---|---|
Symbol.iterator | The iterable protocol | Objects with this method work in for...of, spread, destructuring |
next() | The iterator protocol | Returns {value, done} - the engine calls it repeatedly |
obj[Symbol.iterator] = function* | Make any object iterable | Custom classes gain for...of support |
yield | Pause and produce a value | Generator functions implement the iterator protocol for free |
for...of | Consume any iterable | Arrays, strings, Maps, Sets, generators, NodeList |
[...iterable] | Spread any iterable | Converts iterables to arrays - NodeList to array |
Map / Set built-in iterators | Collection iteration | map.entries() / set.values() return real iterators |
Two protocols power all JavaScript iteration: the ITERABLE protocol (an object has Symbol.iterator returning an iterator) and the ITERATOR protocol (an object has next() returning {value, done}). Generators satisfy both automatically. The table below is the working seven - the pieces that make any custom class work in for...of, spread and destructuring.
Bottom line: implementing ONE method (Symbol.iterator) gives your custom class for...of, spread, destructuring and Array.from support for free. The protocol design means any object can participate in iteration without subclassing Array - you just add the method and the engine handles the rest.
The honest part: the two-protocol design is intentional decoupling. The iterable protocol says 'I can produce an iterator'; the iterator protocol says 'I can produce values on demand'. Separating them means the same iterator can serve multiple consumers, and a lazy generator can produce values only when pulled - the foundation for infinite sequences and streaming data.
How to use
- Add Symbol.iterator to any class: * [Symbol.iterator]() { yield ...; } makes it work in for...of immediately.
- Use yield inside generator functions - the generator implements the iterator protocol automatically.
- Spread any iterable into an array: [...myCustomIterable] for array methods access.
Frequently asked questions
What is the difference between an iterable and an iterator?
An iterable HAS a Symbol.iterator method that RETURNS an iterator; an iterator HAS a next() method that returns {value, done}. The iterable is the container ('I can give you an iterator'); the iterator is the cursor ('I can give you the next value'). Arrays are iterables - calling arr[Symbol.iterator]() returns a fresh iterator. The separation means multiple consumers can each get their own cursor over the same data.
How do I make a custom class iterable?
Add Symbol.iterator as a generator method: class Library { *[Symbol.iterator]() { for (const shelf of this.shelves) yield *shelf.books; } }. The * makes it a generator, and yield * delegates to inner iterables. Now for (const book of library), [...library] and destructuring all work. The generator's lazy evaluation means books are produced one at a time - no intermediate array needed.
What does done: true mean in the iterator protocol?
It signals exhaustion: {value: undefined, done: true} tells the consumer there are no more values. for...of stops on done: true automatically; spread stops too. The subtle part: you can omit value when done is true ({done: true} is valid), and the consumer just sees undefined. After done: true, subsequent next() calls keep returning {done: true} - the iterator is exhausted but not broken.
When should I write a custom iterator instead of a generator?
Almost never in modern code - generators are the ergonomic way to implement the iterator protocol. A generator function automatically returns an object with next(), done and the full iterator contract. Write a raw iterator (with explicit next()) only when you need fine-grained control over the iteration state machine, or when teaching the protocol. In production code, generator functions make custom iteration a one-method implementation.