JavaScript Intermediate #3 Iterators and Generators
After #2 Async Intro, this post covers another of JavaScript’s core tools — iterators and generators.
How does for...of work?
#
In Basics #3 Control Flow you iterated arrays with for...of.
for (const x of [1, 2, 3]) {
console.log(x);
}It works not just on arrays. JavaScript made it so that any object following a certain contract (the iterable protocol) can be iterated with for...of.
for (const ch of 'hello') { // string
console.log(ch);
}
for (const [k, v] of new Map([['a', 1], ['b', 2]])) { // Map
console.log(k, v);
}
for (const x of new Set([1, 2, 3])) { // Set
console.log(x);
}Arrays, strings, Map, Set, and NodeList (document.querySelectorAll), etc. are already iterable. They all follow the same contract.
The iterable protocol #
For an object to be iterable, it must have one thing:
There must be a function under the key
Symbol.iterator. Calling that function must return an iterator withnext().next()must return a{ value, done }object.
Building one is the fastest way to understand it.
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 };
},
};
},
};
for (const n of range) {
console.log(n);
}
// 1, 2, 3, 4, 5
Symbol.iterator is the key. The [...] in the key slot is the computed-property syntax from Basics #5. The special symbol Symbol.iterator becomes the key.
for...of follows this contract internally — calling next() repeatedly until done is true and pulling out values.
Spread and destructuring are iterable too #
Beyond for...of, spread and array destructuring also use the iterable protocol.
const arr = [...range];
// [1, 2, 3, 4, 5]
const [first, second] = range;
// first = 1, second = 2
Math.max(...range); // 5
Array.from(range); // [1, 2, 3, 4, 5]
This is powerful — once you make something iterable, every JavaScript sequence tool works on it.
Generators — short syntax for iterables #
The range above works, but the iterable-creation code is long and intricate. JavaScript has syntax that does the same in one shot — generators.
function* range(from, to) {
for (let n = from; n <= to; n++) {
yield n;
}
}
for (const n of range(1, 5)) {
console.log(n);
}
// 1, 2, 3, 4, 5
[...range(1, 5)]; // [1, 2, 3, 4, 5]
Syntax:
function*— a function with an asterisk → generatoryield— emit a value and pause- On the next call, continue from after
yield
Calling a generator function returns an iterable object. for...of pulls values from it.
Lazy sequences — even infinite ones #
A generator’s real strength — it produces only as needed. Even infinite sequences are expressible.
function* naturals() {
let n = 1;
while (true) {
yield n++;
}
}
const gen = naturals();
gen.next(); // { value: 1, done: false }
gen.next(); // { value: 2, done: false }
gen.next(); // { value: 3, done: false }
// ... forever
while (true) is an infinite loop, but it doesn’t hang. Each next() only produces the next value. Computed only on demand — the meaning of laziness.
Slicing an infinite sequence #
function* take(iterable, n) {
let count = 0;
for (const x of iterable) {
if (count >= n) return;
yield x;
count++;
}
}
const first10 = [...take(naturals(), 10)];
// [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
This is exactly what the take function commonly found in functional libraries does.
Other generator uses #
1) Tree/graph traversal #
function* walk(node) {
yield node.value;
for (const child of node.children ?? []) {
yield* walk(child); // delegate to another generator
}
}
const tree = {
value: 'root',
children: [
{ value: 'a', children: [{ value: 'a1' }] },
{ value: 'b' },
],
};
for (const v of walk(tree)) {
console.log(v);
}
// root, a, a1, b
yield* is syntax for passing through every value of another iterable. Expresses recursive traversal cleanly.
2) Step-by-step procedures #
function* steps() {
console.log('step 1 start');
yield;
console.log('step 2 start');
yield;
console.log('step 3 end');
}
const s = steps();
s.next(); // step 1 start
s.next(); // step 2 start
s.next(); // step 3 end
The caller advances steps explicitly with next(). Sometimes used in tests or simulations.
Async iterators — for await...of
#
A variant for iterating async data. Comes up with streaming data (files, fetch response chunks).
async function readChunks(response) {
for await (const chunk of response.body) {
console.log(chunk);
}
}Detailed in #6 fetch API.
What is Symbol?
#
Symbol.iterator showed up — Symbol is also one of JavaScript’s primitive types. It’s a value that creates a unique identifier.
const s1 = Symbol('id');
const s2 = Symbol('id');
s1 === s2; // false — same description but different symbols
Language-level keys (Symbol.iterator, Symbol.asyncIterator, etc.) are defined as Symbols so they never collide. You won’t create them often in regular code, but libraries use them when attaching metadata to objects without collision.
Wrap-up #
What we covered:
for...of/ spread / destructuring rest on the iterable protocol- To be iterable, return an iterator with
next()fromSymbol.iterator - Build iterables succinctly with generators (
function*,yield) - Lazy sequences — even infinite sequences are expressible
- Delegate to another iterable with
yield* - Iterate async iterables with
for await...of - Symbol — a unique identifier without collisions
In the next post (#4 Destructuring and spread/rest in depth) we go deeper into destructuring and spread that were touched on lightly in Basics — nested patterns, parameter destructuring, dynamic keys, and other practical patterns.