Generators and Iterators in JavaScript
Generators and iterators are powerful features for creating lazy, efficient code. This article covers understanding iteration protocols, creating generators, and practical applications.
Introduction
Generators and iterators enable:
- Lazy evaluation of sequences
- Memory-efficient processing
- Simplified state management
- Elegant async patterns
- Custom iteration logic
Understanding generators helps you:
- Write more efficient code
- Handle large datasets
- Implement complex workflows
- Build powerful abstractions
Iteration Protocol
Iterable Protocol
// An object is iterable if it has a Symbol.iterator method
// that returns an iterator
const iterable = {
[Symbol.iterator]() {
return {
next() {
// Return { value, done }
}
};
}
};
// Can be used in for...of loops
for (const value of iterable) {
console.log(value);
}
Iterator Protocol
// An iterator has a next() method that returns:
// { value: any, done: boolean }
const iterator = {
next() {
return { value: 1, done: false };
}
};
// Call next() to get values
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 1, done: false }
Creating Custom Iterables
// โ
Good: Create a custom iterable
class Range {
constructor(start, end) {
this.start = start;
this.end = end;
}
[Symbol.iterator]() {
let current = this.start;
const end = this.end;
return {
next() {
if (current <= end) {
return { value: current++, done: false };
} else {
return { done: true };
}
}
};
}
}
// Usage
const range = new Range(1, 5);
for (const num of range) {
console.log(num); // 1, 2, 3, 4, 5
}
// Practical example: Iterable for object entries
class ObjectEntries {
constructor(obj) {
this.obj = obj;
}
[Symbol.iterator]() {
const keys = Object.keys(this.obj);
let index = 0;
return {
next: () => {
if (index < keys.length) {
const key = keys[index++];
return {
value: [key, this.obj[key]],
done: false
};
}
return { done: true };
}
};
}
}
// Usage
const obj = { a: 1, b: 2, c: 3 };
const entries = new ObjectEntries(obj);
for (const [key, value] of entries) {
console.log(`${key}: ${value}`);
}
Generator Functions
Basic Generators
// Generator function: uses function* and yield
function* simpleGenerator() {
yield 1;
yield 2;
yield 3;
}
// Calling generator returns an iterator
const gen = simpleGenerator();
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
function* countTo(n) {
for (let i = 1; i <= n; i++) {
yield i;
}
}
// Can be used in for...of
for (const num of countTo(5)) {
console.log(num); // 1, 2, 3, 4, 5
}
// Can be spread
const numbers = [...countTo(5)];
console.log(numbers); // [1, 2, 3, 4, 5]
Generator State
// Generators maintain state between yields
function* counter() {
let count = 0;
while (true) {
yield count++;
}
}
const gen = counter();
console.log(gen.next().value); // 0
console.log(gen.next().value); // 1
console.log(gen.next().value); // 2
Sending Values to Generators
// Send values back to generator with next(value)
function* echo() {
const value1 = yield 'first';
console.log('Received:', value1);
const value2 = yield 'second';
console.log('Received:', value2);
}
const gen = echo();
console.log(gen.next()); // { value: 'first', done: false }
console.log(gen.next('hello')); // Logs: Received: hello
// { value: 'second', done: false }
console.log(gen.next('world')); // Logs: Received: world
// { value: undefined, done: true }
// Practical example: Generator with input
function* fibonacci(limit) {
let a = 0, b = 1;
while (a < limit) {
const next = yield a;
// If value sent, use it; otherwise continue sequence
if (next !== undefined) {
a = next;
} else {
[a, b] = [b, a + b];
}
}
}
const gen = fibonacci(100);
console.log(gen.next().value); // 0
console.log(gen.next().value); // 1
console.log(gen.next().value); // 1
console.log(gen.next(50).value); // 50 (sent value)
console.log(gen.next().value); // 51
Practical Generator Patterns
Lazy Sequences
// โ
Good: Lazy evaluation with generators
function* range(start, end) {
for (let i = start; i <= end; i++) {
yield i;
}
}
function* map(iterable, fn) {
for (const value of iterable) {
yield fn(value);
}
}
function* filter(iterable, predicate) {
for (const value of iterable) {
if (predicate(value)) {
yield value;
}
}
}
// Lazy evaluation: only computes what's needed
const result = filter(
map(range(1, 1000000), x => x * 2),
x => x % 3 === 0
);
// Only processes first 5 values
for (const value of result) {
console.log(value);
if (value > 30) break;
}
Reading Files Line by Line
// โ
Good: Read large files efficiently
async function* readLines(filePath) {
const fs = require('fs');
const readline = require('readline');
const fileStream = fs.createReadStream(filePath);
const rl = readline.createInterface({
input: fileStream,
crlfDelay: Infinity
});
for await (const line of rl) {
yield line;
}
}
// Usage: Process large file without loading into memory
async function processFile(filePath) {
for await (const line of readLines(filePath)) {
console.log(line);
}
}
Infinite Sequences
// โ
Good: Generate infinite sequences
function* infiniteSequence(start = 0, step = 1) {
let current = start;
while (true) {
yield current;
current += step;
}
}
// Take first N values
function* take(iterable, n) {
let count = 0;
for (const value of iterable) {
if (count++ >= n) break;
yield value;
}
}
// Usage
const evens = take(infiniteSequence(0, 2), 5);
console.log([...evens]); // [0, 2, 4, 6, 8]
Delegating to Other Generators
// โ
Good: Use yield* to delegate
function* generator1() {
yield 1;
yield 2;
}
function* generator2() {
yield 3;
yield 4;
}
function* combined() {
yield* generator1();
yield* generator2();
}
console.log([...combined()]); // [1, 2, 3, 4]
// Practical example: Tree traversal
function* traverse(node) {
yield node.value;
if (node.left) {
yield* traverse(node.left);
}
if (node.right) {
yield* traverse(node.right);
}
}
// Usage
const tree = {
value: 1,
left: { value: 2, left: null, right: null },
right: { value: 3, left: null, right: null }
};
for (const value of traverse(tree)) {
console.log(value); // 1, 2, 3
}
Generator Methods
return() Method
// return() terminates the generator
function* gen() {
yield 1;
yield 2;
yield 3;
}
const g = gen();
console.log(g.next()); // { value: 1, done: false }
console.log(g.return(99)); // { value: 99, done: true }
console.log(g.next()); // { value: undefined, done: true }
throw() Method
// throw() injects an error into the generator
function* gen() {
try {
yield 1;
yield 2;
} catch (error) {
console.log('Caught:', error.message);
yield 3;
}
}
const g = gen();
console.log(g.next()); // { value: 1, done: false }
console.log(g.throw(new Error('oops'))); // Logs: Caught: oops
// { value: 3, done: false }
Advanced Patterns
Generator-Based Coroutines
// โ
Good: Use generators for async-like control flow
function* fetchData() {
try {
const response = yield fetch('/api/data');
const data = yield response.json();
console.log('Data:', data);
} catch (error) {
console.error('Error:', error);
}
}
// Runner function to execute generator
function run(generator) {
const iterator = generator();
function handle(result) {
if (result.done) return;
Promise.resolve(result.value)
.then(res => handle(iterator.next(res)))
.catch(err => iterator.throw(err));
}
handle(iterator.next());
}
// Usage
run(fetchData);
State Machine with Generators
// โ
Good: Implement state machine
function* trafficLight() {
while (true) {
yield 'red';
yield 'yellow';
yield 'green';
}
}
const light = trafficLight();
console.log(light.next().value); // red
console.log(light.next().value); // yellow
console.log(light.next().value); // green
console.log(light.next().value); // red
Memoization with Generators
// โ
Good: Cache results with generators
function* memoize(fn) {
const cache = new Map();
while (true) {
const arg = yield;
if (cache.has(arg)) {
console.log('From cache:', arg);
} else {
const result = fn(arg);
cache.set(arg, result);
console.log('Computed:', arg);
}
}
}
const memo = memoize(x => x * 2);
memo.next(); // Start generator
memo.next(5); // Logs: Computed: 5
memo.next(5); // Logs: From cache: 5
Best Practices
-
Use generators for lazy evaluation:
// โ Good function* lazyRange(n) { for (let i = 0; i < n; i++) { yield i; } } -
Delegate with yield:*
// โ Good function* combined() { yield* generator1(); yield* generator2(); } -
Handle errors properly:
// โ Good function* gen() { try { yield value; } catch (error) { // Handle error } } -
Use for large datasets:
// โ Good - memory efficient function* readLargeFile(path) { // Yield one item at a time }
Common Mistakes
-
Forgetting function syntax:*
// โ Bad function gen() { yield 1; } // โ Good function* gen() { yield 1; } -
Not understanding lazy evaluation:
// โ Bad - evaluates all values const values = [1, 2, 3, 4, 5].map(x => x * 2); // โ Good - lazy evaluation function* values() { for (let i = 1; i <= 5; i++) { yield i * 2; } } -
Forgetting generators are iterators:
// โ Bad const gen = generator(); gen.next(); gen.next(); // โ Good - use for...of for (const value of generator()) { console.log(value); }
Summary
Generators and iterators are powerful tools. Key takeaways:
- Iterables have Symbol.iterator method
- Iterators have next() method
- Generators use function* and yield
- Generators are lazy and memory-efficient
- Use yield* to delegate to other generators
- Generators can receive values via next(value)
- Perfect for large datasets and sequences
Related Resources
Next Steps
- Learn about Async Generators and Iteration
- Explore Concurrency Patterns in JavaScript
- Study Advanced Promise Patterns
- Practice with lazy evaluation patterns
- Build custom iterables for your data structures
Comments