JavaScript - Iterables & Generators
Overview
Estimated time: 35–45 minutes
Iterables power for...of
, spread [...x]
, and many APIs. Learn the iterable/iterator protocols, write custom iterables, and use generators (including async generators) to build lazy, composable data pipelines.
Learning Objectives
- Explain the iterable and iterator protocols (
Symbol.iterator
,next()
). - Use
for...of
, spread, and destructuring with iterables correctly. - Implement custom iterables and generators; delegate with
yield*
. - Build lazy pipelines and understand when to prefer iterables over arrays.
- Use async iterables and
for await...of
for streaming and asynchronous sequences.
Prerequisites
- JavaScript - Arrays
- JavaScript - Map & Set (recommended)
Iterable and iterator protocols
An iterable has a [Symbol.iterator]()
method that returns an iterator. An iterator has next()
, returning { value, done }
.
const arr = [1,2,3]; // arrays are iterable
const it = arr[Symbol.iterator]();
it.next(); // { value: 1, done: false }
it.next(); // { value: 2, done: false }
it.next(); // { value: 3, done: false }
it.next(); // { value: undefined, done: true }
// for...of calls [Symbol.iterator]() to get a fresh iterator
for (const x of arr) { /* ... */ }
// Strings are iterable by code points (with 'u' awareness in some APIs)
for (const ch of 'hi') { /* 'h', 'i' */ }
Using iterables
// Spread and destructuring
const set = new Set([3,1,2]);
const a = [...set]; // [3,1,2]
const [first, ...rest] = set; // first=3, rest=[1,2]
// Map/Set iteration yields [key, value] and values respectively
const m = new Map([["x",1],["y",2]]);
for (const [k,v] of m) { /* ... */ }
Custom iterable
// Range iterable as a plain object
const range = (start, end, step = 1) => ({
[Symbol.iterator]() {
let cur = start;
return {
next() {
if ((step > 0 && cur > end) || (step < 0 && cur < end)) return { done: true };
const v = cur; cur += step; return { value: v, done: false };
}
};
}
});
[...range(1,4)]; // [1,2,3,4]
Generators
A generator function (function*
) returns an iterator whose next()
steps through yield
ed values. yield*
delegates to another iterable.
function* genRange(start, end, step = 1) {
for (let x = start; step > 0 ? x <= end : x >= end; x += step) {
yield x;
}
}
// Delegation with yield*
function* oddsUpTo(n) {
for (const x of genRange(1, n)) if (x % 2) yield x;
}
[...oddsUpTo(7)]; // [1,3,5,7]
// Controlling a generator
const g = genRange(1, 2);
g.next(); // { value:1, done:false }
g.return('bye'); // { value:'bye', done:true }
Lazy pipelines with generators
function* map(iter, fn) { for (const x of iter) yield fn(x); }
function* filter(iter, pred) { for (const x of iter) if (pred(x)) yield x; }
function* take(iter, n) { let i=0; for (const x of iter) { if (i++ >= n) return; yield x; } }
const pipeline = take(filter(map(genRange(1, 1000), x => x * 2), x => x % 3 === 0), 5);
[...pipeline]; // [6,12,18,24,30]
Async iterables
Async iterables use [Symbol.asyncIterator]()
and are consumed with for await...of
. Async generators are declared with async function*
.
async function* countAsync(n, delayMs = 200) {
for (let i = 1; i <= n; i++) {
await new Promise(r => setTimeout(r, delayMs));
yield i;
}
}
// Usage (inside an async function):
// for await (const x of countAsync(3)) { console.log(x); }
Common Pitfalls
- One-shot iterators: Some iterables return stateful iterators that cannot be reused once consumed.
- for...in vs for...of:
for...in
iterates keys on objects; usefor...of
for values from iterables. - Infinite generators: Ensure termination or limits when spreading or collecting to arrays.
- Serialization: Iterators/generators are not JSON-serializable; convert to arrays or objects first.
Try it
Run to experiment with custom iterables, generators, and async iteration: