Lesson 14 / 17
Iterators
The iteration protocol's two contracts, making a type traversable through its prototype, the distinction between iterator and iterable, cleanup on early exit, and lazy production.
Contents
In the previous lesson, a for...of attempt on WeakMap threw an error, while the same
loop works on Map, Set, an array, and a string. The difference is not a list of
types: the for...of loop, the spread operator, destructuring, and Array.from all look
at a single contract.
The iteration protocol consists of two contracts. If an object offers a method named
[Symbol.iterator], it is iterable; that method has to return an iterator
offering next. The lesson first sets up the contract by hand, then binds the
measurement series to this contract.
The next Contract
An iterator is an ordinary object with a single method. A next call returns a two-field
object: the next value, and whether the sequence has ended.
function measurementIterator(values) { let index = 0; return { next() { if (index >= values.length) return { value: undefined, done: true }; const value = values[index]; index += 1; return { value, done: false }; }, }; } const iterator = measurementIterator([21.4, 19.8, 25.1]); console.log(JSON.stringify(iterator.next())); console.log(JSON.stringify(iterator.next())); console.log(JSON.stringify(iterator.next())); console.log(JSON.stringify(iterator.next())); const second = measurementIterator([21.4, 19.8]); let step = second.next(); const collected = []; while (!step.done) { collected.push(step.value); step = second.next(); } console.log(collected.join(",")); console.log(typeof iterator[Symbol.iterator]);
{"value":21.4,"done":false}
{"value":19.8,"done":false}
{"value":25.1,"done":false}
{"done":true}
21.4,19.8
undefined
Position information is held in the index variable and is invisible from outside — the
pattern from the Closures lesson works here too. The value field not appearing in the
fourth output is a result of serialization: fields with an undefined value do not enter
JSON.stringify output.
The while loop is a for...of loop written by hand. The last line tells you why this
object cannot be used with for...of: it offers next but not [Symbol.iterator], so
it is an iterator but not iterable.
Making a Type Iterable
The second contract is an object offering a [Symbol.iterator] method. When the method
is written in a class body, it stands on the prototype and all instances become
iterable.
class MeasurementSeries { constructor(sensor, values) { this.sensor = sensor; this.values = values; } [Symbol.iterator]() { let index = 0; const values = this.values; return { next() { if (index >= values.length) return { value: undefined, done: true }; const value = values[index]; index += 1; return { value, done: false }; }, }; } } const series = new MeasurementSeries("S-01", [21.4, 19.8, 25.1]); const collected = []; for (const value of series) collected.push(value); console.log(collected.join(",")); console.log([...series].join(",")); console.log(Array.from(series).length); const [first, second] = series; console.log(`${first} ${second}`); console.log(Math.max(...series)); console.log(Object.hasOwn(series, Symbol.iterator)); console.log(Object.hasOwn(MeasurementSeries.prototype, Symbol.iterator)); console.log(Object.keys(series).join(",")); console.log(JSON.stringify(series));
21.4,19.8,25.1
21.4,19.8,25.1
3
21.4 19.8
25.1
false
true
sensor,values
{"sensor":"S-01","values":[21.4,19.8,25.1]}
One method was added; five separate language features started working. for...of,
spread, Array.from, array destructuring, and argument spreading all use the same
contract.
The sixth and seventh lines tie back to this lesson’s axis: the method stands not on the
instance, but on MeasurementSeries.prototype. Iterability is not a flag belonging to
the object, it is a method found on the chain. This is why making a type iterable
afterward is possible by adding this method to its prototype.
Because it is symbol-keyed, it does not appear in Object.keys output and does not enter
serialization; this property of symbol keys, covered in the Property Descriptors lesson,
keeps protocol methods separate from data fields.
The Iterator–Iterable Distinction
The two contracts most often merge in the same object. Built-in iterators offer a
[Symbol.iterator] method that returns themselves, which is what makes them both
iterator and iterable.
const values = [21.4, 19.8, 25.1]; const iterator = values[Symbol.iterator](); console.log(typeof iterator.next); console.log(typeof iterator[Symbol.iterator]); console.log(iterator[Symbol.iterator]() === iterator); console.log(JSON.stringify(iterator.next())); const remaining = []; for (const value of iterator) remaining.push(value); console.log(remaining.join(",")); const emptyReturn = []; for (const value of iterator) emptyReturn.push(value); console.log(emptyReturn.length); const firstPass = []; for (const value of values) firstPass.push(value); const secondPass = []; for (const value of values) secondPass.push(value); console.log(firstPass.length === secondPass.length); const text = "measurement"; console.log([...text].join("-")); const map = new Map([["S-01", 21.4]]); console.log([...map][0].join("="));
function
function
true
{"value":21.4,"done":false}
19.8,25.1
0
true
m-e-a-s-u-r-e-m-e-n-t
S-01=21.4
The practical consequence of the distinction is exhaustion. Traversing an iterator
consumes it: the first for...of reads the remaining two values, the second finds
nothing. An iterable object, on the other hand, produces a new iterator on every
traversal, which is why it can be traversed over and over.
This explains why it matters whether an iterable object or an iterator is given to a function. If an iterator is given to a function that needs to traverse it twice, the second pass returns empty; no error message is produced either.
The last two lines show how widespread the protocol is. Traversing a string gives code
points, not code units — the distinction introduced in the Character Encodings lesson of
the How Computers Work course is observable here. Traversing a Map gives key–value
pairs.
Early Exit and Cleanup
When a for...of loop is ended with break, or an error is thrown, the protocol calls
the iterator’s return method. This is the reserved point for closing open resources.
const log = []; function resourceIterator(values) { let index = 0; let closed = false; return { [Symbol.iterator]() { return this; }, next() { if (closed || index >= values.length) return { value: undefined, done: true }; const value = values[index]; index += 1; return { value, done: false }; }, return(value) { closed = true; log.push(`resource closed, read=${index}`); return { value, done: true }; }, }; } const resource = resourceIterator([21.4, 19.8, 25.1, 18.2]); for (const value of resource) { if (value > 20 && value < 22) { console.log(`found: ${value}`); break; } } console.log(log.join(" | ")); console.log(JSON.stringify(resource.next())); const secondResource = resourceIterator([21.4, 19.8]); const [singleValue] = secondResource; console.log(singleValue); console.log(log.length);
found: 21.4
resource closed, read=1
{"done":true}
21.4
2
A break statement does not just break the loop, it also notifies the iterator. A closed
iterator answers as finished on subsequent next calls. The last section shows the same
call is made during array destructuring too: once the requested number of values is
taken, the resource is closed.
This behavior matters for iterators bound to resources like file and network streams; early exit does not leave the resource open.
Lazy Production
The protocol’s last property is that values are produced as requested. Even if the source is an array, the computation done on each value only runs when that value is read.
let computeCount = 0; function calibratedSeries(rawValues, offset) { return { [Symbol.iterator]() { let index = 0; return { [Symbol.iterator]() { return this; }, next() { if (index >= rawValues.length) return { value: undefined, done: true }; computeCount += 1; const value = rawValues[index] + offset; index += 1; return { value: Number(value.toFixed(2)), done: false }; }, }; }, }; } const series = calibratedSeries([21.4, 19.8, 25.1, 18.2, 30.0], -0.4); console.log(computeCount); let firstAboveThreshold; for (const value of series) { if (value > 24) { firstAboveThreshold = value; break; } } console.log(firstAboveThreshold); console.log(computeCount); const all = [...series]; console.log(all.join(",")); console.log(computeCount);
0 24.7 3 21,19.4,24.7,17.8,29.6 8
No computation has been done when the series is set up. Three computations run until the first value above the threshold is found; the remaining two values are never computed. When the same series is traversed from the start again, five more computations run and the total climbs to eight.
If the same job were written with map, all five computations would run up front. This
is the lazy evaluation introduced in the Introduction to Functional Programming
lesson of the Programming Fundamentals course, and it is the only workable approach for
large or endless sequences.
The cost of writing the protocol by hand is also visible in this example: position, the
end condition, and the self-returning [Symbol.iterator] method are repeated every
time. The next lesson introduces a syntax that eliminates this repetition.
Summary
- The iteration protocol consists of two contracts: an iterable object offers
[Symbol.iterator], an iterator offersnext. for...of, spread, destructuring,Array.from, and argument spreading all use the same contract; adding a single method makes all of them work.[Symbol.iterator]written in a class body stands on the prototype; iterability is a method found on the chain.- An iterator gets exhausted, an iterable object produces a new iterator on every traversal; this distinction is decisive wherever something needs to be traversed twice.
breakand early destructuring call the iterator’sreturnmethod, allowing for resource cleanup.- Values are produced as requested; an unread value is never computed at all.
Next Step
The same three pieces kept repeating in hand-written iterators: the variable holding
position, the end condition, and the self-returning [Symbol.iterator] method. The
language offers a function type that produces all three itself — functions that can pause
their execution and resume where they left off. The next lesson takes up generators and
reduces this lesson’s lazy series to a few lines.
To keep your progress and take notes, Log in
My notes
Log in to take notes.