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; } } ```javascript - Delegate with yield:*
// ✅ Good function* combined() { yield* generator1(); yield* generator2(); } ```javascript - Handle errors properly:
// ✅ Good function* gen() { try { yield value; } catch (error) { // Handle error } } ```javascript - Use for large datasets:
// ✅ Good - memory efficient function* readLargeFile(path) { // Yield one item at a time } ```javascript
Common Mistakes
- Forgetting function syntax:*
// ❌ Bad function gen() { yield 1; } // ✅ Good function* gen() { yield 1; } ```javascript - 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; } } ```javascript - 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