Advanced JavaScript (ES6+)

Iterators and Generators - Advanced Iteration Control

13 min Lesson 24 of 40

Iterators and Generators - Advanced Iteration Control

Iterators and Generators are powerful features introduced in ES6 that give you fine-grained control over iteration. Iterators define how objects can be looped over, while Generators provide a simple way to create iterators and implement lazy evaluation patterns.

Understanding Iterators

An iterator is an object that implements the iterator protocol by having a next() method that returns an object with two properties:

  • value: The next value in the sequence
  • done: A boolean indicating if the iteration is complete
Key Fact: Arrays, Strings, Maps, Sets, and NodeLists are all built-in iterables. Plain objects are NOT iterable by default.

Creating a Custom Iterator

You can create custom iterators by implementing the iterator protocol:

// Manual iterator const numberIterator = { current: 1, last: 5, next() { if (this.current <= this.last) { return { value: this.current++, done: false }; } else { return { done: true }; } } }; console.log(numberIterator.next()); // {value: 1, done: false} console.log(numberIterator.next()); // {value: 2, done: false} console.log(numberIterator.next()); // {value: 3, done: false} console.log(numberIterator.next()); // {value: 4, done: false} console.log(numberIterator.next()); // {value: 5, done: false} console.log(numberIterator.next()); // {done: true}

Making Objects Iterable

To make an object iterable (usable with for...of), implement the Symbol.iterator method:

// Iterable object const range = { start: 1, end: 5, [Symbol.iterator]() { let current = this.start; const last = this.end; return { next() { if (current <= last) { return { value: current++, done: false }; } return { done: true }; } }; } }; // Now we can use for...of for (const num of range) { console.log(num); // 1, 2, 3, 4, 5 } // And spread operator console.log([...range]); // [1, 2, 3, 4, 5] // And Array.from() console.log(Array.from(range)); // [1, 2, 3, 4, 5] // And destructuring const [first, second, ...rest] = range; console.log(first, second, rest); // 1 2 [3, 4, 5]

Introduction to Generators

Generators are special functions that can pause execution and resume later. They automatically create iterators:

// Generator function (note the asterisk *) function* simpleGenerator() { yield 1; yield 2; yield 3; } 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 for (const value of simpleGenerator()) { console.log(value); // 1, 2, 3 }
Syntax Note: The asterisk (*) can be placed next to function, the function name, or both: function* gen(), function *gen(), or function*gen() are all valid.

Generator Syntax and Features

Generators provide a cleaner way to create iterators:

// Generator that replaces the manual iterator function* rangeGenerator(start, end) { for (let i = start; i <= end; i++) { yield i; } } console.log([...rangeGenerator(1, 5)]); // [1, 2, 3, 4, 5] // Generator with return statement function* generatorWithReturn() { yield 1; yield 2; return 3; // return value is included but done becomes true yield 4; // Never reached } const gen2 = generatorWithReturn(); console.log(gen2.next()); // {value: 1, done: false} console.log(gen2.next()); // {value: 2, done: false} console.log(gen2.next()); // {value: 3, done: true} console.log(gen2.next()); // {value: undefined, done: true} // Note: for...of doesn't include the return value console.log([...generatorWithReturn()]); // [1, 2]

Passing Values to Generators

Generators can receive values through the next() method:

function* twoWayGenerator() { const x = yield 'Give me X'; console.log('X is:', x); const y = yield 'Give me Y'; console.log('Y is:', y); return x + y; } const gen = twoWayGenerator(); console.log(gen.next()); // {value: 'Give me X', done: false} console.log(gen.next(10)); // X is: 10, {value: 'Give me Y', done: false} console.log(gen.next(20)); // Y is: 20, {value: 30, done: true} // Practical example: ID generator function* idGenerator() { let id = 1; while (true) { const increment = yield id; if (increment !== undefined) { id += increment; } else { id++; } } } const ids = idGenerator(); console.log(ids.next().value); // 1 console.log(ids.next().value); // 2 console.log(ids.next(10).value); // 12 console.log(ids.next().value); // 13

Generator Delegation with yield*

Use yield* to delegate to another generator or iterable:

function* gen1() { yield 1; yield 2; } function* gen2() { yield 3; yield 4; } function* combinedGenerator() { yield* gen1(); // Delegate to gen1 yield* gen2(); // Delegate to gen2 yield 5; } console.log([...combinedGenerator()]); // [1, 2, 3, 4, 5] // Delegate to array function* numberAndLetters() { yield* [1, 2, 3]; yield* 'ABC'; } console.log([...numberAndLetters()]); // [1, 2, 3, 'A', 'B', 'C'] // Recursive generator with delegation function* flatten(arr) { for (const item of arr) { if (Array.isArray(item)) { yield* flatten(item); // Recursive delegation } else { yield item; } } } const nested = [1, [2, 3, [4, 5]], 6, [7, [8, 9]]]; console.log([...flatten(nested)]); // [1, 2, 3, 4, 5, 6, 7, 8, 9]

Infinite Sequences with Generators

Generators excel at creating infinite sequences with lazy evaluation:

// Infinite number generator function* infiniteNumbers() { let n = 1; while (true) { yield n++; } } // Take first N values function take(iterable, n) { const result = []; for (const value of iterable) { result.push(value); if (result.length === n) break; } return result; } console.log(take(infiniteNumbers(), 5)); // [1, 2, 3, 4, 5] // Fibonacci sequence function* fibonacci() { let [a, b] = [0, 1]; while (true) { yield a; [a, b] = [b, a + b]; } } console.log(take(fibonacci(), 10)); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34] // Random number generator function* randomNumbers() { while (true) { yield Math.random(); } } console.log(take(randomNumbers(), 3)); // [0.234..., 0.876..., 0.123...]
Warning: Be careful with infinite generators! Always use a termination condition (like take()) or break statement to avoid infinite loops.

Practical Generator Examples

// Example 1: Pagination iterator function* paginate(items, pageSize) { for (let i = 0; i < items.length; i += pageSize) { yield items.slice(i, i + pageSize); } } const data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; for (const page of paginate(data, 3)) { console.log(page); } // [1, 2, 3] // [4, 5, 6] // [7, 8, 9] // [10] // Example 2: Async-like sequential operations function* taskRunner() { console.log('Starting tasks...'); yield 'Task 1 complete'; console.log('Processing...'); yield 'Task 2 complete'; console.log('Finalizing...'); yield 'All tasks complete'; } for (const status of taskRunner()) { console.log(status); } // Example 3: Tree traversal class TreeNode { constructor(value, children = []) { this.value = value; this.children = children; } *[Symbol.iterator]() { yield this.value; for (const child of this.children) { yield* child; } } } const tree = new TreeNode(1, [ new TreeNode(2, [ new TreeNode(4), new TreeNode(5) ]), new TreeNode(3, [ new TreeNode(6) ]) ]); console.log([...tree]); // [1, 2, 4, 5, 3, 6] // Example 4: Lazy map/filter 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; } } } const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; const doubled = map(numbers, x => x * 2); const evens = filter(doubled, x => x % 2 === 0); console.log([...evens]); // [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]

Generator Methods: return() and throw()

Generators have additional methods for control flow:

function* controlledGenerator() { try { yield 1; yield 2; yield 3; } catch (error) { console.log('Caught error:', error); } finally { console.log('Cleanup'); } } // Using return() const gen1 = controlledGenerator(); console.log(gen1.next()); // {value: 1, done: false} console.log(gen1.return(99)); // Cleanup, {value: 99, done: true} console.log(gen1.next()); // {value: undefined, done: true} // Using throw() const gen2 = controlledGenerator(); console.log(gen2.next()); // {value: 1, done: false} console.log(gen2.throw(new Error('Oops!'))); // Caught error: Error: Oops! // Cleanup // {value: undefined, done: true}

Async Generators

ES2018 introduced async generators for working with asynchronous iteration:

async function* asyncGenerator() { yield await Promise.resolve(1); yield await Promise.resolve(2); yield await Promise.resolve(3); } // Using for await...of async function example() { for await (const value of asyncGenerator()) { console.log(value); // 1, 2, 3 } } // Async generator for API pagination async function* fetchPages(url) { let page = 1; let hasMore = true; while (hasMore) { const response = await fetch(`${url}?page=${page}`); const data = await response.json(); yield data.items; hasMore = data.hasNextPage; page++; } } // Usage async function loadAllPages() { for await (const items of fetchPages('/api/data')) { console.log('Page items:', items); } }

Practice Exercise:

Challenge: Create a generator that produces prime numbers.

function* primeNumbers() { yield 2; let num = 3; while (true) { let isPrime = true; for (let i = 2; i <= Math.sqrt(num); i++) { if (num % i === 0) { isPrime = false; break; } } if (isPrime) { yield num; } num += 2; // Skip even numbers } } // Test function take(iterable, n) { const result = []; for (const value of iterable) { result.push(value); if (result.length === n) break; } return result; } console.log(take(primeNumbers(), 10)); // [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]

Try it yourself: Create a generator for perfect squares or powers of 2.

Summary

In this lesson, you learned:

  • Iterators implement the iterator protocol with a next() method
  • Objects become iterable by implementing Symbol.iterator
  • Generators (function*) are special functions that create iterators
  • yield pauses generator execution and produces values
  • Generators support two-way communication via next(value)
  • yield* delegates to other generators or iterables
  • Generators excel at infinite sequences and lazy evaluation
  • Async generators enable asynchronous iteration with for await...of
Next Up: In the next lesson, we'll explore Typed Arrays and ArrayBuffer for binary data handling!