Table of Contents
- Understanding Iterators
- 1.1 The Iterator Protocol
- 1.2 The Iterator Result Object
- 1.3 Example: A Simple Iterator
- Iterable Objects
- 2.1 The Iterable Protocol
- 2.2 Built-in Iterables
- 2.3 Example: Creating a Custom Iterable
- Generators: Simplifying Iterators
- 3.1 Generator Functions (
function*) - 3.2 The Generator Object
- 3.3 The
yieldKeyword: Pause and Resume
- 3.1 Generator Functions (
- Working with Generators
- 4.1 Passing Values with
next() - 4.2 Delegating with
yield* - 4.3 Terminating Generators with
return() - 4.4 Error Handling with
throw()
- 4.1 Passing Values with
- Practical Use Cases
- 5.1 Lazy Evaluation
- 5.2 Infinite Sequences
- 5.3 Simplifying Complex Iteration
- Advanced Example: Fibonacci Sequence Generator
- Common Pitfalls and Best Practices
- Conclusion
- References
1. Understanding Iterators
An iterator is an object that defines a sequence of values and a way to access them one at a time. It adheres to a formal specification called the iterator protocol, which ensures consistency across all iterators in JavaScript.
1.1 The Iterator Protocol
To be an iterator, an object must implement a next() method with the following behavior:
- Returns an object with two properties:
value: The current value in the sequence (can beundefined).done: A boolean (trueif the sequence has ended,falseotherwise).
1.2 The Iterator Result Object
The next() method’s return value is critical. For example:
- When
done: false,valueholds the next item in the sequence. - When
done: true,valueis optional (oftenundefined) and indicates the iterator is exhausted.
1.3 Example: A Simple Iterator
Let’s create a basic iterator to loop through an array. This iterator will return elements one by one until the array is exhausted:
function createArrayIterator(array) {
let index = 0; // Track position in the array
return {
next: function() {
if (index < array.length) {
// Return next value and increment index
return { value: array[index++], done: false };
} else {
// No more values: done is true
return { done: true };
}
}
};
}
// Usage
const fruits = ['apple', 'banana', 'cherry'];
const fruitIterator = createArrayIterator(fruits);
console.log(fruitIterator.next()); // { value: 'apple', done: false }
console.log(fruitIterator.next()); // { value: 'banana', done: false }
console.log(fruitIterator.next()); // { value: 'cherry', done: false }
console.log(fruitIterator.next()); // { done: true }
Here, fruitIterator is an iterator. Each call to next() advances the sequence until done: true.
2. Iterable Objects
An iterable is an object that can be iterated over (e.g., with for...of loops). To be iterable, it must implement the iterable protocol, which requires a method at the key Symbol.iterator that returns an iterator.
2.1 The Iterable Protocol
The iterable protocol states:
- An object is iterable if it has a
[Symbol.iterator]()method. [Symbol.iterator]()must return a valid iterator (adhering to the iterator protocol).
2.2 Built-in Iterables
JavaScript has many built-in iterables, including:
- Arrays (
[]) - Strings (
"") - Maps (
new Map()) - Sets (
new Set()) - Typed arrays (
Uint8Array, etc.)
These objects work with for...of loops because they implement [Symbol.iterator](). For example:
// Iterate over a string (built-in iterable)
for (const char of 'hello') {
console.log(char); // 'h', 'e', 'l', 'l', 'o'
}
// Iterate over a Map (built-in iterable)
const myMap = new Map([['a', 1], ['b', 2]]);
for (const [key, value] of myMap) {
console.log(`${key}: ${value}`); // 'a: 1', 'b: 2'
}
2.3 Example: Creating a Custom Iterable
Let’s define a custom iterable object. For example, a Range object that generates numbers from start to end:
const Range = {
start: 1,
end: 5,
// Implement the iterable protocol
[Symbol.iterator]() {
let current = this.start; // Track current value
const end = this.end;
return {
next: function() {
if (current <= end) {
return { value: current++, done: false };
} else {
return { done: true };
}
}
};
}
};
// Now Range is iterable: use with for...of
for (const num of Range) {
console.log(num); // 1, 2, 3, 4, 5
}
Here, Range implements [Symbol.iterator](), making it iterable. The for...of loop automatically calls [Symbol.iterator]() to get an iterator, then calls next() until done: true.
3. Generators: Simplifying Iterators
While iterators are powerful, manually implementing next() and tracking state (like index or current) can be tedious. Generators solve this by letting you write iterators with a simpler, function-like syntax.
3.1 Generator Functions (function*)
A generator function is defined with function* (note the asterisk) and uses the yield keyword to pause execution and return values. When called, it returns a generator object (an iterator and iterable).
3.2 The Generator Object
The generator object is both an iterator (has next()) and an iterable (has [Symbol.iterator]()). This means it works with for...of loops out of the box.
3.3 The yield Keyword: Pause and Resume
The yield keyword is unique to generators. It:
- Pauses the generator function.
- Returns a value to the caller (via the iterator result
{ value, done }). - Resumes execution when
next()is called again.
Example of a simple generator:
// Generator function
function* numberGenerator() {
console.log('Generator started');
yield 1; // Pause, return 1
console.log('Resumed after first yield');
yield 2; // Pause, return 2
console.log('Resumed after second yield');
yield 3; // Pause, return 3
console.log('Generator finished');
}
// Call generator function to get generator object
const generator = numberGenerator();
// Generator is paused initially: no output yet
console.log(generator.next());
// Logs: "Generator started" → { value: 1, done: false }
console.log(generator.next());
// Logs: "Resumed after first yield" → { value: 2, done: false }
console.log(generator.next());
// Logs: "Resumed after second yield" → { value: 3, done: false }
console.log(generator.next());
// Logs: "Generator finished" → { done: true }
Key observations:
- The generator starts paused;
numberGenerator()doesn’t run untilnext()is called. - Each
yieldpauses execution, andnext()resumes it.
4. Working with Generators
Generators offer more than just simple iteration. Let’s explore advanced features like passing values to generators, delegating to other iterables, and error handling.
4.1 Passing Values with next()
You can pass values into a generator using next(value). The passed value becomes the result of the yield expression that paused the generator.
Example:
function* echoGenerator() {
const input1 = yield 'Waiting for first input...'; // Pause, return message
yield `You said: ${input1}`; // Return input1
const input2 = yield 'Waiting for second input...'; // Pause again
yield `You also said: ${input2}`; // Return input2
}
const echo = echoGenerator();
console.log(echo.next());
// { value: 'Waiting for first input...', done: false }
console.log(echo.next('hello'));
// input1 = 'hello' → { value: 'You said: hello', done: false }
console.log(echo.next('world'));
// input2 = 'world' → { value: 'You also said: world', done: false }
console.log(echo.next());
// { done: true }
4.2 Delegating with yield*
Use yield* to delegate iteration to another iterable (e.g., an array, another generator, or a built-in iterable). This “flattens” the delegated sequence into the current generator.
Example:
function* generatorA() {
yield 'A1';
yield 'A2';
}
function* generatorB() {
yield 'B1';
yield* generatorA(); // Delegate to generatorA
yield 'B2';
}
// Iterate over generatorB
const gen = generatorB();
console.log(gen.next().value); // 'B1'
console.log(gen.next().value); // 'A1' (from generatorA)
console.log(gen.next().value); // 'A2' (from generatorA)
console.log(gen.next().value); // 'B2'
4.3 Terminating Generators with return()
Generators have a return(value) method to immediately terminate the sequence and return { value, done: true }.
Example:
function* myGenerator() {
yield 1;
yield 2;
yield 3;
}
const gen = myGenerator();
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.return('Done!')); // { value: 'Done!', done: true }
console.log(gen.next()); // { done: true } (no more values)
4.4 Error Handling with throw()
Use throw(error) to pass an error into the generator. If the generator has a try/catch block around the yield, it can catch the error and resume.
Example:
function* errorGenerator() {
try {
yield 'Working...';
yield 'Still working...';
} catch (e) {
yield `Caught error: ${e}`; // Handle error and resume
}
yield 'Recovered!';
}
const gen = errorGenerator();
console.log(gen.next()); // { value: 'Working...', done: false }
console.log(gen.throw('Oops!')); // { value: 'Caught error: Oops!', done: false }
console.log(gen.next()); // { value: 'Recovered!', done: false }
5. Practical Use Cases
Generators and iterators shine in scenarios where traditional loops fall short. Here are key use cases:
5.1 Lazy Evaluation
Lazy evaluation processes data on demand, avoiding unnecessary computation. Generators are perfect for this because they yield values one at a time, not all at once.
Example: Process a large dataset without loading it all into memory:
function* processLargeFile(fileLines) {
for (const line of fileLines) {
if (line.includes('error')) {
yield line; // Only yield lines with "error" (lazy)
}
}
}
// Simulate a large file (10,000 lines)
const largeFile = Array.from({ length: 10000 }, (_, i) =>
i % 100 === 0 ? `error: line ${i}` : `line ${i}`
);
// Process errors lazily (only when needed)
const errorLines = processLargeFile(largeFile);
console.log(errorLines.next().value); // 'error: line 0'
console.log(errorLines.next().value); // 'error: line 100'
// ... and so on
5.2 Infinite Sequences
Generators can produce infinite sequences because they pause after each yield. You can iterate until you meet a condition (e.g., a maximum value).
Example: Generate prime numbers infinitely:
function isPrime(n) {
for (let i = 2; i <= Math.sqrt(n); i++) {
if (n % i === 0) return false;
}
return n > 1;
}
function* infinitePrimes() {
let num = 2;
while (true) { // Infinite loop!
if (isPrime(num)) {
yield num; // Yield prime, then pause
}
num++;
}
}
// Get the first 5 primes
const primes = infinitePrimes();
for (let i = 0; i < 5; i++) {
console.log(primes.next().value); // 2, 3, 5, 7, 11
}
5.3 Simplifying Complex Iteration
Generators simplify iteration logic for nested or hierarchical data (e.g., trees, nested arrays).
Example: Flatten a nested array:
function* flattenArray(arr) {
for (const item of arr) {
if (Array.isArray(item)) {
yield* flattenArray(item); // Recursively delegate
} else {
yield item; // Yield non-array items
}
}
}
const nestedArray = [1, [2, [3, 4], 5], 6];
const flattened = flattenArray(nestedArray);
console.log(Array.from(flattened)); // [1, 2, 3, 4, 5, 6]
6. Advanced Example: Fibonacci Sequence Generator
The Fibonacci sequence (0, 1, 1, 2, 3, 5, …) is a classic example of an infinite sequence. Let’s implement it with a generator:
function* fibonacci() {
let a = 0, b = 1;
while (true) { // Infinite loop
yield a; // Yield current Fibonacci number
[a, b] = [b, a + b]; // Update for next iteration
}
}
// Get the first 10 Fibonacci numbers
const fib = fibonacci();
const first10Fib = Array.from({ length: 10 }, () => fib.next().value);
console.log(first10Fib); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
This generator runs infinitely but only computes values when next() is called—perfectly lazy!
7. Common Pitfalls and Best Practices
-
Generators are paused initially: A generator function doesn’t execute until
next()is called.function* logGenerator() { console.log('Hello'); } const gen = logGenerator(); // No log yet! gen.next(); // Logs "Hello" -
for...ofconsumes the iterator: Once a generator is exhausted (done: true), it can’t be restarted. Create a new generator instance if you need to re-iterate. -
Avoid side effects: Generators should focus on yielding values, not modifying external state (unless intentional).
-
Handle errors: Always use
try/catchin generators if you expectthrow()to be called.
8. Conclusion
Iterators and generators are powerful additions to JavaScript, enabling flexible, lazy, and readable iteration. Iterators provide a standard way to traverse sequences, while generators simplify creating iterators with pause/resume capabilities.
Key takeaways:
- Iterators follow the iterator protocol (
next()returning{ value, done }). - Iterables implement
[Symbol.iterator]()and work withfor...of. - Generators (defined with
function*) useyieldto pause/resume, making them ideal for lazy evaluation and infinite sequences.
By mastering these tools, you’ll write cleaner code for complex iteration tasks, from processing large datasets to generating infinite sequences.