An iterator is the machinery behind every for...of loop. It is an object with one job: each time you call its next() method, it hands you the next value and tells you whether it is finished. Understanding this lets you make your own objects loopable.
Every call to next() returns an object with two properties:
| value | The next item |
| done | false while there are more items, true when finished |
Ask an array for its iterator, then call next() yourself:
const colors = ["red", "green"];
const it = colors[Symbol.iterator]();
console.log(it.next());
console.log(it.next());
console.log(it.next());
That is exactly what for...of does for you: get the iterator, call next() over and over, run the loop body with each value, and stop when done is true.
// What for...of does, written out by hand
const iter = colors[Symbol.iterator]();
let step = iter.next();
while (!step.done) {
console.log(step.value);
step = iter.next();
}
An iterator only moves forward. Once it reaches the end, it stays finished:
const it = new Set([1, 2]).values();
console.log([...it]); // [1, 2]
console.log([...it]); // [] already exhausted
set.values() or map.entries() in a variable and loop over it twice, the second loop sees nothing. Call the method again to get a fresh iterator.
Any object with a working next() method is an iterator. Here is a home-made one that counts by tens forever:
function myNumbers() {
let n = 0;
return {
next() {
n += 10;
return { value: n, done: false };
}
};
}
const n = myNumbers();
console.log(n.next().value, n.next().value, n.next().value);
for (const x of n) { }
Calling next() by hand works, but for...of refuses. It does not look for next(); it looks for a [Symbol.iterator] method that returns an iterator. An object needs both halves to work with for...of, which is exactly what the next example builds.
Give an object a [Symbol.iterator] method that returns an iterator, and suddenly for...of, spread, and Array.from all work on 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);
console.log([...range]);
Each time a loop starts, it calls [Symbol.iterator]() again and gets a fresh iterator with current back at the start. That is why range can be looped over as many times as you like.
[Symbol.iterator]() are a computed method name (from Object Properties). Symbol.iterator is a special built-in key that JavaScript looks for; you cannot type it as a plain name.
One more rule: once done is true, value is ignored, so you can leave it out and return just { done: true }. That also means a value sent with done: true is never seen by for...of or spread.
myNumbers = {}; and done = false; with no let or const. Those are automatic globals (JS Variables), and in strict mode (JS Strict Mode) the code crashes with ReferenceError: myNumbers is not defined. It also returns { value: 100, done: true } at the end, so its loop stops at 90: the 100 is thrown away. And it says Symbol.iterator "returns a next() function"; it actually returns an object that has a next() method.
| Iterable | Iterator | |
|---|---|---|
| Has | [Symbol.iterator]() | next() |
| Job | Hand out a new iterator | Hand out values one by one |
| Reusable | Yes | No, used up once |
| Example | an array | array.values() |
Think of an iterable as a book and an iterator as a bookmark. Anyone can open the book with a new bookmark; each bookmark only moves forward through the pages.
Iterators used to have only next(). To filter or transform one, you had to spread it into an array first. ECMAScript 2025 added iterator helpers: methods like the array ones you already know, built right into iterators. They work in current browsers (Chrome since 2024).
Iterator.from() turns any iterable (or iterator) into an iterator that has the helpers:
const it = Iterator.from([1, 2, 3, 4, 5, 6]);
const result = it
.filter(x => x % 2 === 0) // 2, 4, 6
.map(x => x * 10) // 20, 40, 60
.take(2); // just the first two
console.log(result.toArray());
Iterators from Sets, Maps, and array methods like values() already have the helpers, so you can skip Iterator.from() there: new Set(["a", "b"]).values().map(s => s.toUpperCase()).
| Helper | Returns | Example | Result |
|---|---|---|---|
| map(fn) | iterator | [1,2,3] map x => x * 2 | 2, 4, 6 |
| filter(fn) | iterator | [32,33,16,40] filter x => x > 18 | 32, 33, 40 |
| flatMap(fn) | iterator | [1,2,3] flatMap x => [x, x * 10] | 1, 10, 2, 20, 3, 30 |
| take(n) | iterator | [1,2,3,4,5,6] take(5) | 1, 2, 3, 4, 5 |
| drop(n) | iterator | [1,2,3,4,5,6] drop(5) | 6 |
| find(fn) | value | [3,10,18,30,20] find x => x > 18 | 30 |
| some(fn) | true / false | "123456789" some x => x > 7 | true |
| every(fn) | true / false | "123456789" every x => x > 7 | false |
| reduce(fn, start) | value | [175,50,25] reduce (a, b) => a - b | 100 |
| forEach(fn) | undefined | runs fn once per value | |
| toArray() | array | collects every remaining value |
The first five are lazy: they return a new iterator and do no work until something asks for values. That is why take() is safe even on a sequence that never ends (you will build one in the next lesson).
[1, 2, 3].drop(1) throws TypeError: [1,2,3].drop is not a function. Arrays have their own map and filter but no drop or take. Wrap the array first: Iterator.from([1, 2, 3]).drop(1). And because map, filter, take, and friends return an iterator, logging one shows an Iterator object, not your values. Finish with .toArray() or spread it.
find() stops at the first match, and the iterator stays where it stopped:
const it = Iterator.from([1, 2, 3, 4]);
console.log(it.find(x => x > 1)); // 2
console.log([...it]); // [3, 4]: 1 and 2 are gone
W3Schools' drop() example stores myIterator.drop(5) in a variable named firstFive. Its comment is right (it removes the first five), but the name is backwards: what is left is [6].
next() by hand is a lot of code for a simple idea. Generators, in the next lesson, produce iterators with far less typing.
An object has a working next() method but no [Symbol.iterator]. What happens with for (const x of obj)?
next()TypeError: obj is not iterablefor...of looks for [Symbol.iterator], not next(). The object is an iterator but not an iterable.What does this log?
const it = Iterator.from([5, 10, 15, 20]);
console.log(it.drop(1).map(x => x / 5).toArray());
[2, 3, 4]. drop(1) skips the 5, then map divides 10, 15, and 20 by 5.Why can take(3) be used on a sequence that never ends, when toArray() cannot?
take is fastertake is lazy: it only pulls 3 values, while toArray tries to collect every valuetoArray only works on arraystake, map, filter, drop, and flatMap do no work until values are requested. toArray() keeps asking until done, which never comes.next() until done is truemap.keys() in a variable, spread it twice, and explain the second resultrange example with its Try it Yourself button and loop over it twicerange so it counts by twos (1, 3, 5...)