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!