Skip to main content

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

  1. Create a custom iterator for a data structure
  2. Build a generator-based range function
  3. Implement lazy map/filter/reduce
  4. Create an infinite sequence generator
  5. Build a tree traversal iterator
  6. Implement async data fetching with generators
  7. Create generator utilities library
  8. Build a stream processing pipeline

Additional Resources