Module 24: Iterators and Generators
Iterators and Generators provide powerful mechanisms for creating custom iteration behavior and lazy evaluation in JavaScript.
1. Iterators
1.1 Understanding Iterators
// Array is iterable
const arr = [1, 2, 3];
// Get iterator
const iterator = arr[Symbol.iterator]();
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }
console.log(iterator.next()); // { value: undefined, done: true }
1.2 Iterator Protocol
// Iterator must have next() method
const iterator = {
current: 0,
last: 5,
next() {
if (this.current <= this.last) {
return { value: this.current++, done: false };
}
return { value: undefined, done: true };
}
};
console.log(iterator.next()); // { value: 0, done: false }
console.log(iterator.next()); // { value: 1, done: false }
// ... continues until done: true
1.3 Making Objects Iterable
// Custom iterable object
const range = {
from: 1,
to: 5,
[Symbol.iterator]() {
let current = this.from;
const last = this.to;
return {
next() {
if (current <= last) {
return { value: current++, done: false };
}
return { value: undefined, done: true };
}
};
}
};
// Now can use for...of
for (let num of range) {
console.log(num); // 1, 2, 3, 4, 5
}
// Can use spread operator
console.log([...range]); // [1, 2, 3, 4, 5]
// Can use Array.from()
console.log(Array.from(range)); // [1, 2, 3, 4, 5]
2. Generators
2.1 Basic Generator
// Generator function
function* numberGenerator() {
yield 1;
yield 2;
yield 3;
}
const gen = numberGenerator();
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 }
// Can iterate with for...of
for (let num of numberGenerator()) {
console.log(num); // 1, 2, 3
}
2.2 Generator Syntax
// Function declaration
function* gen1() { yield 1; }
// Function expression
const gen2 = function*() { yield 2; };
// Method in object
const obj = {
*gen3() { yield 3; }
};
// Arrow functions CANNOT be generators
// const gen4 = *() => { yield 4; }; // ❌ Syntax error
2.3 yield Expression
function* generator() {
console.log('Start');
const x = yield 1;
console.log('x:', x);
const y = yield 2;
console.log('y:', y);
return 3;
}
const gen = generator();
console.log(gen.next()); // "Start", { value: 1, done: false }
console.log(gen.next(10)); // "x: 10", { value: 2, done: false }
console.log(gen.next(20)); // "y: 20", { value: 3, done: true }
3. Generator Patterns
3.1 Range Generator
function* range(start, end, step = 1) {
for (let i = start; i <= end; i += step) {
yield i;
}
}
console.log([...range(1, 5)]); // [1, 2, 3, 4, 5]
console.log([...range(0, 10, 2)]); // [0, 2, 4, 6, 8, 10]
console.log([...range(10, 0, -2)]); // [10, 8, 6, 4, 2, 0]
// Infinite range
function* infiniteRange(start = 0) {
let i = start;
while (true) {
yield i++;
}
}
// Take first 5
const gen = infiniteRange();
const first5 = Array.from({ length: 5 }, () => gen.next().value);
console.log(first5); // [0, 1, 2, 3, 4]
3.2 ID Generator
function* idGenerator() {
let id = 1;
while (true) {
yield id++;
}
}
const ids = idGenerator();
console.log(ids.next().value); // 1
console.log(ids.next().value); // 2
console.log(ids.next().value); // 3
3.3 Fibonacci Generator
function* fibonacci() {
let [prev, curr] = [0, 1];
while (true) {
yield curr;
[prev, curr] = [curr, prev + curr];
}
}
// Get first 10 fibonacci numbers
const fib = fibonacci();
const first10 = Array.from({ length: 10 }, () => fib.next().value);
console.log(first10); // [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
4. Generator Delegation
4.1 yield*
function* gen1() {
yield 1;
yield 2;
}
function* gen2() {
yield 'a';
yield* gen1(); // Delegate to gen1
yield 'b';
}
console.log([...gen2()]); // ['a', 1, 2, 'b']
4.2 Nested Iteration
function* flatten(arr) {
for (let item of arr) {
if (Array.isArray(item)) {
yield* flatten(item); // Recursive delegation
} else {
yield item;
}
}
}
const nested = [1, [2, [3, [4]], 5]];
console.log([...flatten(nested)]); // [1, 2, 3, 4, 5]
5. Lazy Evaluation
5.1 Performance Benefits
// Eager evaluation (processes all)
function eagerMap(array, fn) {
return array.map(fn);
}
// Lazy evaluation (processes on demand)
function* lazyMap(iterable, fn) {
for (let item of iterable) {
yield fn(item);
}
}
// Example: only process what you need
const numbers = Array.from({ length: 1000000 }, (_, i) => i);
const eager = eagerMap(numbers, x => x * 2); // Processes all 1M immediately
const lazy = lazyMap(numbers, x => x * 2); // Processes on demand
// Only compute first 5
const first5 = Array.from({ length: 5 }, () => lazy.next().value);
console.log(first5); // [0, 2, 4, 6, 8]
5.2 Infinite Sequences
function* naturals() {
let n = 1;
while (true) {
yield n++;
}
}
function* take(iterable, count) {
let i = 0;
for (let item of iterable) {
if (i++ >= count) break;
yield item;
}
}
function* filter(iterable, predicate) {
for (let item of iterable) {
if (predicate(item)) {
yield item;
}
}
}
// Chain operations lazily
const evenNaturals = filter(naturals(), n => n % 2 === 0);
const first10Evens = take(evenNaturals, 10);
console.log([...first10Evens]); // [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
6. Generator Methods
6.1 return()
function* gen() {
yield 1;
yield 2;
yield 3;
}
const g = gen();
console.log(g.next()); // { value: 1, done: false }
console.log(g.return(999)); // { value: 999, done: true }
console.log(g.next()); // { value: undefined, done: true }
6.2 throw()
function* gen() {
try {
yield 1;
yield 2;
yield 3;
} catch (e) {
console.log('Caught:', e);
}
}
const g = gen();
console.log(g.next()); // { value: 1, done: false }
console.log(g.throw('Error!')); // "Caught: Error!", { value: undefined, done: true }
7. Async Iterators and Generators
7.1 Async Iterators
// Async iterable object
const asyncRange = {
from: 1,
to: 3,
[Symbol.asyncIterator]() {
let current = this.from;
const last = this.to;
return {
async next() {
// Simulate async operation
await new Promise(resolve => setTimeout(resolve, 1000));
if (current <= last) {
return { value: current++, done: false };
}
return { value: undefined, done: true };
}
};
}
};
// Use with for await...of
(async () => {
for await (let num of asyncRange) {
console.log(num); // 1, 2, 3 (with 1s delay each)
}
})();
7.2 Async Generators
async function* asyncGenerator() {
yield await Promise.resolve(1);
yield await Promise.resolve(2);
yield await Promise.resolve(3);
}
(async () => {
for await (let num of asyncGenerator()) {
console.log(num); // 1, 2, 3
}
})();
7.3 Fetching Data
async function* fetchPages(url, maxPages) {
let page = 1;
while (page <= maxPages) {
const response = await fetch(`${url}?page=${page}`);
const data = await response.json();
yield data;
page++;
}
}
// Usage
(async () => {
for await (let pageData of fetchPages('/api/items', 3)) {
console.log('Page data:', pageData);
}
})();
8. Practical Examples
8.1 Pagination Iterator
class PaginatedData {
constructor(data, pageSize) {
this.data = data;
this.pageSize = pageSize;
}
*[Symbol.iterator]() {
for (let i = 0; i < this.data.length; i += this.pageSize) {
yield this.data.slice(i, i + this.pageSize);
}
}
}
const items = Array.from({ length: 25 }, (_, i) => i + 1);
const paginated = new PaginatedData(items, 10);
for (let page of paginated) {
console.log('Page:', page);
}
// Page: [1, 2, ..., 10]
// Page: [11, 12, ..., 20]
// Page: [21, 22, ..., 25]
8.2 Stream Processing
function* readLines(text) {
const lines = text.split('\n');
for (let line of lines) {
yield line.trim();
}
}
function* filterNonEmpty(lines) {
for (let line of lines) {
if (line) {
yield line;
}
}
}
function* parseData(lines) {
for (let line of lines) {
const [name, value] = line.split(':');
yield { name: name.trim(), value: value.trim() };
}
}
// Process stream
const data = `
name: John
age: 30
city: NYC
`;
const lines = readLines(data);
const nonEmpty = filterNonEmpty(lines);
const parsed = parseData(nonEmpty);
console.log([...parsed]);
// [{ name: 'name', value: 'John' }, ...]
8.3 Tree Traversal
class TreeNode {
constructor(value, children = []) {
this.value = value;
this.children = children;
}
// Depth-first traversal
*[Symbol.iterator]() {
yield this.value;
for (let child of this.children) {
yield* child;
}
}
// Breadth-first traversal
*breadthFirst() {
const queue = [this];
while (queue.length > 0) {
const node = queue.shift();
yield node.value;
queue.push(...node.children);
}
}
}
const tree = new TreeNode(1, [
new TreeNode(2, [
new TreeNode(4),
new TreeNode(5)
]),
new TreeNode(3, [
new TreeNode(6)
])
]);
console.log('Depth-first:', [...tree]); // [1, 2, 4, 5, 3, 6]
console.log('Breadth-first:', [...tree.breadthFirst()]); // [1, 2, 3, 4, 5, 6]
9. Generator Utilities
9.1 take()
function* take(iterable, count) {
let i = 0;
for (let item of iterable) {
if (i++ >= count) break;
yield item;
}
}
function* naturals() {
let n = 1;
while (true) yield n++;
}
console.log([...take(naturals(), 5)]); // [1, 2, 3, 4, 5]
9.2 filter()
function* filter(iterable, predicate) {
for (let item of iterable) {
if (predicate(item)) {
yield item;
}
}
}
const numbers = [1, 2, 3, 4, 5, 6];
const evens = filter(numbers, n => n % 2 === 0);
console.log([...evens]); // [2, 4, 6]
9.3 map()
function* map(iterable, fn) {
for (let item of iterable) {
yield fn(item);
}
}
const numbers = [1, 2, 3, 4, 5];
const doubled = map(numbers, n => n * 2);
console.log([...doubled]); // [2, 4, 6, 8, 10]
9.4 chain()
function* chain(...iterables) {
for (let iterable of iterables) {
yield* iterable;
}
}
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const arr3 = [7, 8, 9];
console.log([...chain(arr1, arr2, arr3)]); // [1, 2, 3, 4, 5, 6, 7, 8, 9]
9.5 zip()
function* zip(...iterables) {
const iterators = iterables.map(i => i[Symbol.iterator]());
while (true) {
const results = iterators.map(i => i.next());
if (results.some(r => r.done)) {
break;
}
yield results.map(r => r.value);
}
}
const names = ['John', 'Jane', 'Bob'];
const ages = [30, 25, 35];
const cities = ['NYC', 'LA', 'Chicago'];
for (let [name, age, city] of zip(names, ages, cities)) {
console.log(`${name}, ${age}, ${city}`);
}
// John, 30, NYC
// Jane, 25, LA
// Bob, 35, Chicago
10. Best Practices
10.1 When to Use Generators
// ✅ Good use cases:
// - Lazy evaluation of large datasets
// - Infinite sequences
// - Complex iteration logic
// - Memory-efficient processing
// ❌ Avoid for:
// - Simple iterations (use for...of)
// - When you need all values at once (use map/filter)
10.2 Error Handling
function* safeGenerator() {
try {
yield 1;
yield 2;
throw new Error('Something went wrong');
yield 3; // Never reached
} catch (e) {
console.error('Error:', e.message);
yield 'error';
} finally {
console.log('Cleanup');
}
}
console.log([...safeGenerator()]);
// Error: Something went wrong
// Cleanup
// [1, 2, 'error']
10.3 Generator Composition
// ✅ Compose small generators
function* numbers() {
yield* [1, 2, 3, 4, 5];
}
function* doubled(gen) {
for (let n of gen()) {
yield n * 2;
}
}
function* filtered(gen, predicate) {
for (let n of gen()) {
if (predicate(n)) {
yield n;
}
}
}
const result = filtered(
() => doubled(numbers),
n => n > 5
);
console.log([...result]); // [6, 8, 10]
Performance
Generators provide:
- Memory efficiency: Values computed on demand
- Lazy evaluation: Only compute what you need
- Infinite sequences: Represent unbounded data
- Clean syntax: Simplify complex iteration
Summary
In this module, you learned:
- ✅ Iterator protocol and Symbol.iterator
- ✅ Generator functions and yield
- ✅ Generator delegation with yield*
- ✅ Lazy evaluation patterns
- ✅ Async iterators and generators
- ✅ Practical patterns: pagination, streams, trees
- ✅ Generator utilities: take, filter, map, chain, zip
- ✅ Best practices for generators
Next Steps
In Module 25, you'll learn about Regular Expressions for pattern matching.
Practice Exercises
- Create a custom iterator for a data structure
- Build a generator-based range function
- Implement lazy map/filter/reduce
- Create an infinite sequence generator
- Build a tree traversal iterator
- Implement async data fetching with generators
- Create generator utilities library
- Build a stream processing pipeline