The Iterator Protocol
Any object that implements a next() method returning { value, done } is an iterator. Any object with [Symbol.iterator]() that returns an iterator is an iterable.
// Manual iterator implementation
function makeCounter(start = 0, end = Infinity) {
let current = start;
return {
next() {
if (current <= end) {
return { value: current++, done: false };
}
return { value: undefined, done: true };
}
};
}
const counter = makeCounter(1, 3);
console.log(counter.next()); // { value: 1, done: false }
console.log(counter.next()); // { value: 2, done: false }
console.log(counter.next()); // { value: 3, done: false }
console.log(counter.next()); // { value: undefined, done: true }
// Built-in iterators - arrays, strings, Maps, Sets are all iterable
const arr = [10, 20, 30];
const iter = arr[Symbol.iterator](); // get the iterator
console.log(iter.next()); // { value: 10, done: false }
console.log(iter.next()); // { value: 20, done: false }
// for...of uses the iterator protocol under the hood
for (const num of arr) {
console.log(num); // 10, 20, 30
}
// Destructuring and spread also use the iterator protocol
const [first, ...rest] = arr; // first=10, rest=[20,30]
const copy = [...arr]; // [10, 20, 30]
Generator Basics
A generator function (function*) returns a generator object that is both an iterator and an iterable. It pauses at each yield.
// Generator function - note the * after function
function* simpleGenerator() {
console.log('Before first yield');
yield 1;
console.log('Before second yield');
yield 2;
console.log('Before return');
return 3; // done: true
}
const gen = simpleGenerator(); // does NOT run the function body yet
console.log(gen.next()); // logs "Before first yield" -> { value: 1, done: false }
console.log(gen.next()); // logs "Before second yield" -> { value: 2, done: false }
console.log(gen.next()); // logs "Before return" -> { value: 3, done: true }
console.log(gen.next()); // { value: undefined, done: true }
// Generator is also iterable - use with for...of, spread, destructuring
function* colors() {
yield 'red';
yield 'green';
yield 'blue';
}
for (const color of colors()) {
console.log(color); // red, green, blue
}
// for...of ignores the return value (done: true) - only gets yield values
const colorArray = [...colors()]; // ['red', 'green', 'blue']
const [first] = colors(); // 'red'
yield and Return
// yield* - delegate to another iterable
function* inner() {
yield 'a';
yield 'b';
}
function* outer() {
yield 1;
yield* inner(); // yields 'a', then 'b' inline
yield* [10, 20]; // yield* works with any iterable
yield 2;
}
console.log([...outer()]); // [1, 'a', 'b', 10, 20, 2]
// Passing values INTO a generator via next(value)
function* calculator() {
const a = yield 'Enter first number:';
const b = yield 'Enter second number:';
yield `Result: ${a + b}`;
}
const calc = calculator();
console.log(calc.next().value); // 'Enter first number:'
console.log(calc.next(5).value); // 'Enter second number:' (5 assigned to a)
console.log(calc.next(3).value); // 'Result: 8' (3 assigned to b)
// generator.return(value) - force-close the generator
function* counter() {
let i = 0;
while (true) yield i++;
}
const c = counter();
c.next(); // { value: 0, done: false }
c.return('stopped'); // { value: 'stopped', done: true }
c.next(); // { value: undefined, done: true } - generator is closed
// generator.throw(error) - inject an error at the yield point
function* safe() {
try {
yield 1;
} catch (e) {
console.log('Caught:', e.message);
yield 'recovered';
}
}
const s = safe();
s.next(); // { value: 1, done: false }
s.throw(new Error('oops')); // logs 'Caught: oops' -> { value: 'recovered', done: false }
Infinite Sequences
Generators are perfect for infinite sequences because values are produced lazily - only when requested. The sequence never materializes in memory all at once.
// Infinite integer sequence
function* integers(start = 0, step = 1) {
let n = start;
while (true) {
yield n;
n += step;
}
}
// Take first N values from an infinite generator
function take(gen, n) {
const result = [];
for (const value of gen) {
result.push(value);
if (result.length === n) break; // break closes the generator
}
return result;
}
console.log(take(integers(0, 2), 5)); // [0, 2, 4, 6, 8]
console.log(take(integers(10), 4)); // [10, 11, 12, 13]
// Fibonacci sequence
function* fibonacci() {
let [a, b] = [0, 1];
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
console.log(take(fibonacci(), 8)); // [0, 1, 1, 2, 3, 5, 8, 13]
// ID generator - useful for generating unique IDs
function* idGenerator(prefix = 'id') {
let n = 1;
while (true) yield `${prefix}-${n++}`;
}
const nextId = idGenerator('user');
console.log(nextId.next().value); // 'user-1'
console.log(nextId.next().value); // 'user-2'
Symbol.iterator
Adding [Symbol.iterator]() to any object makes it work with for...of, spread, and destructuring.
// Make a custom object 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 };
return { value: undefined, done: true };
}
};
}
}
const range = new Range(1, 5);
for (const n of range) console.log(n); // 1 2 3 4 5
console.log([...range]); // [1, 2, 3, 4, 5]
const [a, b, ...rest] = range; // a=1, b=2, rest=[3,4,5]
// Simpler: use a generator as [Symbol.iterator]
class Range2 {
constructor(start, end) { this.start = start; this.end = end; }
*[Symbol.iterator]() { // generator method shorthand
for (let i = this.start; i <= this.end; i++) yield i;
}
}
// Tree traversal with a generator
class TreeNode {
constructor(value, left = null, right = null) {
this.value = value;
this.left = left;
this.right = right;
}
*[Symbol.iterator]() { // in-order traversal
if (this.left) yield* this.left;
yield this.value;
if (this.right) yield* this.right;
}
}
const tree = new TreeNode(4,
new TreeNode(2, new TreeNode(1), new TreeNode(3)),
new TreeNode(6, new TreeNode(5), new TreeNode(7))
);
console.log([...tree]); // [1, 2, 3, 4, 5, 6, 7]
Practical Generator Patterns
// Pattern 1: Lazy map/filter for large datasets
function* lazyMap(iterable, fn) {
for (const value of iterable) yield fn(value);
}
function* lazyFilter(iterable, pred) {
for (const value of iterable) if (pred(value)) yield value;
}
const millionNumbers = integers(1);
const pipeline = lazyFilter(lazyMap(millionNumbers, x => x * x), x => x % 3 === 0);
console.log(take(pipeline, 5)); // [9, 36, 81, 144, 225] - computed lazily
// Pattern 2: Chunking - split an array into batches
function* chunks(array, size) {
for (let i = 0; i < array.length; i += size) {
yield array.slice(i, i + size);
}
}
for (const batch of chunks([1,2,3,4,5,6,7], 3)) {
console.log(batch); // [1,2,3], [4,5,6], [7]
}
// Pattern 3: Zip multiple iterables together
function* zip(...iterables) {
const iters = iterables.map(it => it[Symbol.iterator]());
while (true) {
const results = iters.map(it => it.next());
if (results.some(r => r.done)) return;
yield results.map(r => r.value);
}
}
const names = ['Alice', 'Bob', 'Carol'];
const scores = [95, 87, 91];
for (const [name, score] of zip(names, scores)) {
console.log(`${name}: ${score}`); // Alice: 95, Bob: 87, Carol: 91
}
Async Generators
Combine async and function* to produce values asynchronously. Use for await...of to consume them.
// Async generator - can use await inside
async function* fetchPages(baseUrl) {
let page = 1;
while (true) {
const res = await fetch(`${baseUrl}?page=${page}`);
const data = await res.json();
if (data.items.length === 0) return; // no more pages
yield data.items;
if (!data.hasMore) return;
page++;
}
}
// Consume with for await...of
async function loadAllProducts() {
const allProducts = [];
for await (const page of fetchPages('/api/products')) {
allProducts.push(...page);
console.log(`Loaded ${allProducts.length} products`);
}
return allProducts;
}
// Async generator for real-time streaming (Server-Sent Events)
async function* readSSE(url) {
const response = await fetch(url);
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { value, done } = await reader.read();
if (done) return;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop(); // keep incomplete line
for (const line of lines) {
if (line.startsWith('data: ')) {
yield JSON.parse(line.slice(6));
}
}
}
}
for await (const event of readSSE('/api/stream')) {
console.log('Event:', event);
}