Have you ever wondered how for...of loops work? Or why you can spread arrays but not plain objects? The answer lies in JavaScript's iteration protocol—a powerful mechanism that enables consistent iteration across different data structures. Let's explore iterators and generators to understand this fundamental concept.
The Iteration Protocol
JavaScript defines two protocols that make objects iterable:
- Iterable Protocol: An object is iterable if it has a
[Symbol.iterator]method that returns an iterator - Iterator Protocol: An iterator is an object with a
next()method that returns{ value, done }
flowchart TD
A[Iterable Object] -->|"[Symbol.iterator]()"| B[Iterator]
B -->|"next()"| C["{value: 1, done: false}"]
B -->|"next()"| D["{value: 2, done: false}"]
B -->|"next()"| E["{value: undefined, done: true}"]
style A fill:#3b82f6,color:#fff
style B fill:#8b5cf6,color:#fff
style C fill:#10b981,color:#fff
style D fill:#10b981,color:#fff
style E fill:#f59e0b,color:#fff
// Arrays are iterable
const arr = [1, 2, 3];
const iterator = arr[Symbol.iterator]();
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }
console.log(iterator.next()); // { value: undefined, done: true }
Built-in Iterables
JavaScript has several built-in iterable objects:
// Arrays
for (const item of [1, 2, 3]) {
console.log(item);
}
// Strings
for (const char of "Hello") {
console.log(char);
}
// Maps
const map = new Map([["a", 1], ["b", 2]]);
for (const [key, value] of map) {
console.log(key, value);
}
// Sets
const set = new Set([1, 2, 3]);
for (const item of set) {
console.log(item);
}
// Arguments object
function example() {
for (const arg of arguments) {
console.log(arg);
}
}
Creating Custom Iterables
You can make any object iterable by implementing the iteration protocol:
const range = {
start: 1,
end: 5,
[Symbol.iterator]() {
let current = this.start;
const end = this.end;
return {
next() {
if (current <= end) {
return { value: current++, done: false };
}
return { value: undefined, done: true };
}
};
}
};
for (const num of range) {
console.log(num); // 1, 2, 3, 4, 5
}
// Also works with spread
console.log([...range]); // [1, 2, 3, 4, 5]
Making a Class Iterable
class Playlist {
constructor() {
this.songs = [];
}
add(song) {
this.songs.push(song);
return this;
}
[Symbol.iterator]() {
let index = 0;
const songs = this.songs;
return {
next() {
if (index < songs.length) {
return { value: songs[index++], done: false };
}
return { done: true };
}
};
}
}
const playlist = new Playlist()
.add("Song A")
.add("Song B")
.add("Song C");
for (const song of playlist) {
console.log(`Now playing: ${song}`);
}
Generator Functions
Generator functions provide an easier way to create iterators. They use the function* syntax and yield keyword:
flowchart LR
subgraph Generator Function
A["function* gen()"] --> B["yield 1"]
B --> C["yield 2"]
C --> D["yield 3"]
D --> E["return"]
end
subgraph Execution
F["gen()"] --> G["Iterator"]
G -->|"next()"| H["1"]
G -->|"next()"| I["2"]
G -->|"next()"| J["3"]
end
style A fill:#8b5cf6,color:#fff
style B fill:#10b981,color:#fff
style C fill:#10b981,color:#fff
style D fill:#10b981,color:#fff
function* numberGenerator() {
yield 1;
yield 2;
yield 3;
}
const gen = numberGenerator();
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: 3, done: false }
console.log(gen.next()); // { value: undefined, done: true }
// Generators are iterable
for (const num of numberGenerator()) {
console.log(num); // 1, 2, 3
}
Simplified Range with Generator
The range example becomes much simpler with a generator:
function* range(start, end) {
for (let i = start; i <= end; i++) {
yield i;
}
}
console.log([...range(1, 5)]); // [1, 2, 3, 4, 5]
for (const num of range(10, 15)) {
console.log(num); // 10, 11, 12, 13, 14, 15
}
Lazy Evaluation
One of the most powerful features of generators is lazy evaluation—values are computed only when needed:
function* infiniteSequence() {
let n = 0;
while (true) {
yield n++;
}
}
// This doesn't hang! Values are generated on demand
const infinite = infiniteSequence();
console.log(infinite.next().value); // 0
console.log(infinite.next().value); // 1
console.log(infinite.next().value); // 2
// Take first 5 values
function* take(iterable, count) {
let i = 0;
for (const item of iterable) {
if (i >= count) return;
yield item;
i++;
}
}
console.log([...take(infiniteSequence(), 5)]); // [0, 1, 2, 3, 4]
Fibonacci Sequence
function* fibonacci() {
let [prev, curr] = [0, 1];
while (true) {
yield curr;
[prev, curr] = [curr, prev + curr];
}
}
// Get first 10 Fibonacci numbers
const fib = fibonacci();
const first10 = [];
for (let i = 0; i < 10; i++) {
first10.push(fib.next().value);
}
console.log(first10); // [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
yield* for Delegation
The yield* expression delegates to another iterable or generator:
function* inner() {
yield 2;
yield 3;
}
function* outer() {
yield 1;
yield* inner(); // Delegate to inner generator
yield 4;
}
console.log([...outer()]); // [1, 2, 3, 4]
// Flatten nested arrays
function* flatten(arr) {
for (const item of arr) {
if (Array.isArray(item)) {
yield* flatten(item); // Recursive delegation
} else {
yield item;
}
}
}
const nested = [1, [2, [3, 4]], 5];
console.log([...flatten(nested)]); // [1, 2, 3, 4, 5]
Two-Way Communication
Generators can receive values through next():
function* conversation() {
const name = yield "What is your name?";
const hobby = yield `Hello, ${name}! What's your hobby?`;
yield `${hobby} sounds fun, ${name}!`;
}
const talk = conversation();
console.log(talk.next().value); // "What is your name?"
console.log(talk.next("Alice").value); // "Hello, Alice! What's your hobby?"
console.log(talk.next("coding").value);// "coding sounds fun, Alice!"
Practical Use Cases
Paginated API Data
async function* fetchPages(baseUrl) {
let page = 1;
let hasMore = true;
while (hasMore) {
const response = await fetch(`${baseUrl}?page=${page}`);
const data = await response.json();
yield data.items;
hasMore = data.hasNextPage;
page++;
}
}
// Usage
async function processAllPages() {
for await (const items of fetchPages("/api/products")) {
for (const item of items) {
console.log(item);
}
}
}
Tree Traversal
function* traverseTree(node) {
yield node.value;
if (node.children) {
for (const child of node.children) {
yield* traverseTree(child);
}
}
}
const tree = {
value: 1,
children: [
{ value: 2, children: [{ value: 4 }, { value: 5 }] },
{ value: 3, children: [{ value: 6 }] }
]
};
console.log([...traverseTree(tree)]); // [1, 2, 4, 5, 3, 6]
ID Generator
function* idGenerator(prefix = "id") {
let id = 1;
while (true) {
yield `${prefix}-${id++}`;
}
}
const userIdGen = idGenerator("user");
const orderIdGen = idGenerator("order");
console.log(userIdGen.next().value); // "user-1"
console.log(userIdGen.next().value); // "user-2"
console.log(orderIdGen.next().value); // "order-1"
Iterables vs Arrays
flowchart TD
A{Need all values at once?}
A -->|Yes| B[Use Array]
A -->|No| C{Potentially infinite?}
C -->|Yes| D[Use Generator]
C -->|No| E{Memory constrained?}
E -->|Yes| D
E -->|No| F[Either works]
style B fill:#3b82f6,color:#fff
style D fill:#10b981,color:#fff
style F fill:#f59e0b,color:#fff
| Feature | Array | Iterator/Generator |
|---|---|---|
| Memory | Stores all values | Values on demand |
| Infinite sequences | Not possible | Possible |
| Multiple iterations | Yes | Single pass only |
| Random access | Yes (arr[i]) |
No |
Summary
- The iteration protocol consists of iterables (with
[Symbol.iterator]) and iterators (withnext()) - Built-in iterables include arrays, strings, Maps, Sets, and more
- Create custom iterables by implementing
[Symbol.iterator]() - Generator functions (
function*) provide an easier way to create iterators yieldpauses execution and produces a valueyield*delegates to another iterable- Generators enable lazy evaluation and can handle infinite sequences
- Use
for await...offor async iteration
Understanding iterators and generators unlocks powerful patterns for working with sequences of data, from simple transformations to complex async data streams.
References
- Flanagan, David. JavaScript: The Definitive Guide, 7th Edition. O'Reilly Media, 2020.
- Haverbeke, Marijn. Eloquent JavaScript, 4th Edition. No Starch Press, 2024.