JavaScript Iterators and Generators
Introduction to Iterators
Iterators are a powerful JavaScript feature that provide a standard way to produce a sequence of values. They are objects that implement the iterator protocol by having a next()
method that returns an object with two properties:
value
: The current value in the sequencedone
: A boolean indicating whether the sequence has been exhausted
The Iterator Protocol
An object is an iterator when it implements the next()
method with the following semantics:
// Basic iterator implementation
const myIterator = {
data: [1, 2, 3, 4, 5],
currentIndex: 0,
// The next() method returns an object with two properties
next() {
if (this.currentIndex < this.data.length) {
return {
value: this.data[this.currentIndex++], // Return current value and increment index
done: false // Sequence not finished
};
} else {
return { done: true }; // Sequence is finished
}
}
};
// Using the iterator
console.log(myIterator.next()); // { value: 1, done: false }
console.log(myIterator.next()); // { value: 2, done: false }
console.log(myIterator.next()); // { value: 3, done: false }
console.log(myIterator.next()); // { value: 4, done: false }
console.log(myIterator.next()); // { value: 5, done: false }
console.log(myIterator.next()); // { done: true }
Iterable Objects
An iterable is an object that implements the iterable protocol by having a method with the key Symbol.iterator
that returns an iterator.
Built-in Iterables
Many built-in types in JavaScript are iterable:
- Arrays
- Strings
- Maps
- Sets
- DOM collections (NodeList, HTMLCollection)
- arguments object
Creating Custom Iterables
// Custom iterable object
const myIterable = {
data: [10, 20, 30, 40, 50],
// Symbol.iterator method returns an iterator
[Symbol.iterator]() {
let index = 0;
const data = this.data;
return {
next() {
if (index < data.length) {
return {
value: data[index++],
done: false
};
} else {
return { done: true };
}
}
};
}
};
// Using the iterable with for...of loop
for (const item of myIterable) {
console.log(item); // 10, 20, 30, 40, 50
}
// Using the spread operator
const values = [...myIterable]; // [10, 20, 30, 40, 50]
// Using destructuring
const [first, second, ...rest] = myIterable; // first=10, second=20, rest=[30,40,50]
Iteration Protocols in Action
The following example demonstrates a practical use case for custom iterables - a range function similar to Python's:
// Range function that returns an iterable
function range(start, end, step = 1) {
return {
[Symbol.iterator]() {
let current = start;
return {
next() {
if ((step > 0 && current <= end) || (step < 0 && current >= end)) {
const value = current;
current += step;
return { value, done: false };
} else {
return { done: true };
}
}
};
}
};
}
// Using the range iterable
for (const num of range(1, 10, 2)) {
console.log(num); // 1, 3, 5, 7, 9
}
// Count down
for (const num of range(10, 1, -2)) {
console.log(num); // 10, 8, 6, 4, 2
}
for...of
loops, spread syntax, destructuring, and more.
Introduction to Generators
Generators are a special type of function that can be paused and resumed, making them perfect for creating iterators with less boilerplate code. They are defined using a function with an asterisk (function*
) and use the yield
keyword to pause execution and return values.
Basic Generator Syntax
// Simple generator function
function* simpleGenerator() {
yield 1;
yield 2;
yield 3;
}
// Using the generator
const generator = simpleGenerator();
console.log(generator.next()); // { value: 1, done: false }
console.log(generator.next()); // { value: 2, done: false }
console.log(generator.next()); // { value: 3, done: false }
console.log(generator.next()); // { value: undefined, done: true }
// Generators are iterables
for (const value of simpleGenerator()) {
console.log(value); // 1, 2, 3
}
Generator Features
Generators have several powerful features that make them unique:
- Lazy Evaluation: Values are computed on-demand, not all at once
- Memory Efficiency: Great for working with large or infinite sequences
- Two-way Communication: Can pass values back into the generator with
next(value)
- Control Flow: Makes complex asynchronous code more readable
Implementing the Range Example with Generators
0) {
while (current <= end) {
yield current;
current += step;
}
} else {
while (current >= end) {
yield current;
current += step;
}
}
}
// Using the range generator
for (const num of rangeGenerator(1, 10, 2)) {
console.log(num); // 1, 3, 5, 7, 9
}]]>
Advanced Generator Techniques
Two-way Communication
Generators allow two-way communication: not only can they yield values to the caller, but they can also receive values from the caller using next(value)
.
function* twoWayCommunication() {
// First yield doesn't receive any value
const a = yield 'First question?';
console.log('Received:', a);
const b = yield 'Second question?';
console.log('Received:', b);
return 'All done!';
}
const gen = twoWayCommunication();
// First next() call starts the generator
console.log(gen.next()); // { value: 'First question?', done: false }
// Second next() call sends 'Answer 1' to the generator
console.log(gen.next('Answer 1')); // Logs "Received: Answer 1"
// Returns { value: 'Second question?', done: false }
// Third next() call sends 'Answer 2' to the generator
console.log(gen.next('Answer 2')); // Logs "Received: Answer 2"
// Returns { value: 'All done!', done: true }
Error Handling in Generators
Generators support error handling with try/catch
blocks and the throw()
method:
function* errorHandlingGenerator() {
try {
yield 1;
yield 2;
yield 3;
} catch (error) {
console.log('Caught error:', error);
yield 'Error recovered';
}
yield 4;
}
const gen = errorHandlingGenerator();
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }
// Throw an error into the generator
console.log(gen.throw('Something went wrong!'));
// Logs "Caught error: Something went wrong!"
// Returns { value: 'Error recovered', done: false }
console.log(gen.next()); // { value: 4, done: false }
console.log(gen.next()); // { value: undefined, done: true }
Generator Delegation with yield*
You can delegate to other generators using the yield*
expression:
function* generateNumbers() {
yield 1;
yield 2;
}
function* generateLetters() {
yield 'a';
yield 'b';
}
function* generateAll() {
yield* generateNumbers();
yield* generateLetters();
yield* [3, 4]; // Can also delegate to any iterable
}
for (const value of generateAll()) {
console.log(value); // 1, 2, 'a', 'b', 3, 4
}
Practical Applications
Infinite Sequences
Generators are perfect for creating infinite sequences because they compute values on-demand:
// Infinite Fibonacci sequence generator
function* fibonacci() {
let [prev, curr] = [0, 1];
while (true) {
yield curr;
[prev, curr] = [curr, prev + curr];
}
}
// Take first 10 Fibonacci numbers
const fib = fibonacci();
for (let i = 0; i < 10; i++) {
console.log(fib.next().value); // 1, 1, 2, 3, 5, 8, 13, 21, 34, 55
}
Data Processing Pipelines
Generators can be used to create data processing pipelines:
// Generator pipeline for processing data
function* numbers() {
yield* [1, 2, 3, 4, 5];
}
function* double(iterable) {
for (const value of iterable) {
yield value * 2;
}
}
function* filter(predicate, iterable) {
for (const value of iterable) {
if (predicate(value)) {
yield value;
}
}
}
// Create a pipeline: numbers -> double -> filter even numbers
const pipeline = filter(
x => x % 2 === 0,
double(numbers())
);
// Process the data
for (const value of pipeline) {
console.log(value); // 4, 8
}
Async Operations with Generators
Before async/await
, generators were used for handling asynchronous operations:
// Simple generator-based async runner
function runGenerator(generatorFunc) {
const generator = generatorFunc();
function handle(result) {
if (result.done) return Promise.resolve(result.value);
return Promise.resolve(result.value)
.then(res => handle(generator.next(res)))
.catch(err => handle(generator.throw(err)));
}
return handle(generator.next());
}
// Using the runner with a generator function
runGenerator(function* () {
try {
// Simulating async operations
const users = yield fetch('https://jsonplaceholder.typicode.com/users');
const userData = yield users.json();
console.log('Users:', userData);
const posts = yield fetch('https://jsonplaceholder.typicode.com/posts');
const postData = yield posts.json();
console.log('Posts:', postData);
return 'All data loaded';
} catch (error) {
console.error('Error:', error);
}
}).then(result => console.log(result));
async/await
for this purpose. The example above is for educational purposes to show how generators can control flow.
Performance Considerations
Memory Efficiency
One of the biggest advantages of generators is memory efficiency when working with large data sets:
// Memory-intensive approach (creates a large array in memory)
function getLargeArray(size) {
const result = [];
for (let i = 0; i < size; i++) {
result.push(i);
}
return result;
}
// Memory-efficient approach (generates values on demand)
function* getLargeGenerator(size) {
for (let i = 0; i < size; i++) {
yield i;
}
}
// Using a large array (stores all values in memory at once)
// const largeArray = getLargeArray(1000000); // Uses ~8MB of memory
// largeArray.forEach(num => {
// if (num % 1000000 === 0) console.log(num);
// });
// Using a generator (computes values on demand)
// const largeGen = getLargeGenerator(1000000); // Minimal memory usage
// for (const num of largeGen) {
// if (num % 1000000 === 0) console.log(num);
// }
Generator Overhead
While generators are memory-efficient, they do have some performance overhead:
- Each
yield
involves suspending and resuming execution - For simple operations on small data sets, regular functions might be faster
- The benefits of generators increase with larger data sets or more complex operations
Browser Support and Compatibility
Iterators and generators have excellent support in modern browsers:
- Chrome: Full support since version 39
- Firefox: Full support since version 26
- Safari: Full support since version 10
- Edge: Full support since version 13
For older browsers, you can use transpilers like Babel to convert generators to ES5-compatible code.