Generators & Iterators
JavaScript provides powerful features for dealing with collections of data and custom iteration logic: Iterators and Generators. These tools allow you to pause execution, handle infinite sequences, and build custom data structures effortlessly.
1. The Iterator Protocol
An iterator is an object that provides a next() method. This method returns an object with two properties:
value: The next value in the iteration sequence.done: A boolean indicating whether the iteration is complete.
function makeRangeIterator(start = 0, end = Infinity, step = 1) {
let nextIndex = start;
let iterationCount = 0;
return {
next() {
if (nextIndex < end) {
const result = { value: nextIndex, done: false };
nextIndex += step;
iterationCount++;
return result;
}
return { value: iterationCount, done: true };
}
};
}
const it = makeRangeIterator(1, 4);
console.log(it.next()); // { value: 1, done: false }
console.log(it.next()); // { value: 2, done: false }
console.log(it.next()); // { value: 3, done: false }
console.log(it.next()); // { value: 3, done: true }
2. Iterables and Symbol.iterator
An object is iterable if it defines its iteration behavior. To be iterable, an object must implement the [Symbol.iterator]() method, which returns an iterator.
Built-in iterables include Array, String, Map, Set, etc. You can make custom objects iterable:
const myIterable = {
*[Symbol.iterator]() {
yield 1;
yield 2;
yield 3;
}
};
for (const value of myIterable) {
console.log(value); // 1, 2, 3
}
3. Generators (function*)
While custom iterators are powerful, creating them manually requires maintaining internal state. Generators provide a much cleaner syntax to build iterators. A generator function is declared with function* and uses the yield keyword to pause and return values.
function* simpleGenerator() {
yield 'A';
yield 'B';
yield 'C';
}
const gen = simpleGenerator();
console.log(gen.next().value); // 'A'
console.log(gen.next().value); // 'B'
Yielding Values
The yield keyword pauses the generator's execution and sends a value back to the caller. When next() is called again, execution resumes immediately after the last yield.
Yielding to other Generators (yield*)
The yield* expression is used to delegate to another iterable or generator object.
function* generateSequence() {
yield 1;
yield 2;
}
function* generateCombined() {
yield* generateSequence();
yield 3;
}
console.log([...generateCombined()]); // [1, 2, 3]
4. Infinite Sequences
Because generators compute values on demand (lazy evaluation), they are perfect for representing infinite sequences, such as a never-ending unique ID generator.
function* idMaker() {
let index = 0;
while (true) {
yield index++;
}
}
const ids = idMaker();
console.log(ids.next().value); // 0
console.log(ids.next().value); // 1
console.log(ids.next().value); // 2
Summary
Generators and Iterators are advanced patterns that enable custom iteration behaviors, state machine creation, and efficient memory usage when dealing with large or infinite datasets.