Iterators
An iterator is a small object that knows how to produce values one at a time. JavaScript's iteration features — for...of, spread, destructuring, Array.from — all sit on top of one tiny contract called the iterator protocol. Understanding it unlocks generators, custom iterables, and a much clearer mental model of how loops actually work.
The iterator protocol
An iterator is any object with a next() method that returns { value, done }:
value— the next item.done—falsewhile more values exist,truewhen iteration is finished. Whendoneistrue,valueis usuallyundefined.
Driving an array iterator by hand
const arr = ["a", "b", "c"];
const it = arr[Symbol.iterator]();
console.log(it.next()); // { value: 'a', done: false }
console.log(it.next()); // { value: 'b', done: false }
console.log(it.next()); // { value: 'c', done: false }
console.log(it.next()); // { value: undefined, done: true }{ value: 'a', done: false }
{ value: 'b', done: false }
{ value: 'c', done: false }
{ value: undefined, done: true }The iterable protocol
An iterable is any object that has a method at the well-known symbol Symbol.iterator which returns a fresh iterator. for...of and spread look for that method first — anything that has it can be iterated.
function isIterable(x) {
return x != null && typeof x[Symbol.iterator] === "function";
}
isIterable([1, 2]); // true
isIterable("hello"); // true
isIterable(new Set()); // true
isIterable({ a: 1 }); // falsePlain objects are not iterable by default. Arrays, strings, Map, Set, NodeList, typed arrays, generators and arguments all are.
Consuming with for...of
for...of does the protocol work for you: it calls Symbol.iterator, calls next() repeatedly, and stops when done is true.
What for...of is doing under the hood
function forEach(iterable, fn) {
const it = iterable[Symbol.iterator]();
while (true) {
const { value, done } = it.next();
if (done) break;
fn(value);
}
}
forEach(["a", "b", "c"], console.log);a b c
Building a custom iterable
Make any object iterable by adding a [Symbol.iterator] method. It must return an iterator — an object with next().
A range that knows how to iterate itself
function range(start, end, step = 1) {
return {
[Symbol.iterator]() {
let current = start;
return {
next() {
if (current < end) {
const value = current;
current += step;
return { value, done: false };
}
return { value: undefined, done: true };
},
};
},
};
}
for (const n of range(0, 5)) console.log(n);
console.log([...range(0, 5)]);
console.log([...range(0, 10, 2)]);0 1 2 3 4 [ 0, 1, 2, 3, 4 ] [ 0, 2, 4, 6, 8 ]
Iterables that produce themselves
An iterator can also be an iterable — return this from its Symbol.iterator method. That's how built-in iterators like array.entries() work: they can be used directly with for...of.
function counter(max) {
let i = 0;
return {
next() {
return i < max ? { value: i++, done: false } : { value: undefined, done: true };
},
[Symbol.iterator]() { return this; },
};
}
for (const n of counter(3)) console.log(n);0 1 2
Where iterators show up
Once an object is iterable, all of these features just work:
for...ofloops.Array/spread destructuring:
const [a, b] = iterable.Spread into arrays or arguments:
[...iterable],Math.max(...iterable).Array.from(iterable)andArray.from(iterable, fn).new Map(iterable),new Set(iterable)— when the items are pairs or values.Promise.all(iterable)and friends.
Early termination — return()
Iterators may also implement an optional return() method. It is called by for...of when the loop exits early (break, throw, return), giving the iterator a chance to clean up — close a file handle, release a resource, finish a transaction.
function cleanupRange(n) {
let i = 0;
return {
next() {
return i < n ? { value: i++, done: false } : { value: undefined, done: true };
},
return(value) {
console.log("cleanup at", i);
return { value, done: true };
},
[Symbol.iterator]() { return this; },
};
}
for (const n of cleanupRange(10)) {
if (n === 3) break;
}cleanup at 4
Lazy by nature
Iterators only produce values when asked. That makes them ideal for infinite or expensive sequences — you don't materialise the whole thing into an array.
const naturals = {
[Symbol.iterator]() {
let i = 1;
return { next() { return { value: i++, done: false }; } };
},
};
// Pull the first five — the iterator stops when we stop asking.
const it = naturals[Symbol.iterator]();
for (let i = 0; i < 5; i++) console.log(it.next().value);1 2 3 4 5
Writing iterators by hand is verbose. The next page, Generators, shows how function* and yield let you write iterators that look like ordinary code.