You've used for...of on arrays, strings, Sets and Maps, and spread syntax like [...set] to turn things into arrays. But how do they know how to loop over such different kinds of data? They all follow the same simple agreement, the iteration protocol: an object is iterable if it can hand out an iterator, and an iterator is anything with a next() method that returns the values one at a time.
Once you understand that agreement, you can make your own objects work with for...of, spread and destructuring. Generator functions, written function*, make this remarkably easy: they can pause in the middle with yield, hand back a value, and carry on from the same spot next time. That makes them perfect for sequences, step-by-step processes and even infinite lists that are calculated only when needed.
The key that connects everything is a symbol, Symbol.iterator. Symbols are a primitive type designed for unique property keys that never clash. In this lesson you'll learn all three ideas, and see how they lead into async iteration with async/await.
The iteration protocol
const it = ["a", "b"][Symbol.iterator]();
it.next(); // { value: "a", done: false }
it.next(); // { value: "b", done: false }
it.next(); // { value: undefined, done: true }- An iterator has a
next()method that returns{ value, done }. - An iterable has a
[Symbol.iterator]()method that returns a new iterator. for...of, spread[...x], destructuringconst [a, b] = x,Array.from(x),new Set(x)andPromise.all(x)all work with any iterable.- Plain objects aren't iterable, which is why
for...ofon{}throws an error.
Generator functions
function* countTo(n) {
for (let i = 1; i <= n; i++) {
yield i; // pause here and hand out i
}
}
for (const x of countTo(3)) console.log(x); // 1, 2, 3function*declares a generator. Calling it doesn't run the body; it returns a generator object, which is both an iterator and an iterable.- Each
next()runs the body until the nextyield, returns that value, and pauses. - When the function ends,
donebecomestrue. yield*hands out every value from another iterable:yield* [1, 2, 3].
Lazy and infinite sequences
Generators only work out the next value when it's asked for. That makes infinite sequences possible:
function* ids() { let n = 1; while (true) yield `id-${n++}`; }
const next = ids();
next.next().value; // "id-1"
next.next().value; // "id-2"The while (true) is safe because the generator pauses at every yield. Just never spread an infinite generator into an array. Newer browsers add iterator helpers such as .take(5), .map() and .filter() directly on iterators, so you can work with lazy sequences without creating arrays.
Symbols
A symbol is a unique value created with Symbol("description"). No two symbols are ever equal, even with the same description, so they make property keys that can't clash with anyone else's:
const id = Symbol("id");
const user = { name: "Ana", [id]: 42 };
user[id]; // 42
Object.keys(user); // ["name"]: symbol keys are skippedJavaScript uses built-in well-known symbols to let you customise behaviour: Symbol.iterator for looping, Symbol.asyncIterator for for await...of, and Symbol.toPrimitive for converting objects to numbers or strings.
JavaScript Iterators and Generators Syntax
Iterators and generators
function* gen() { yield 1; yield 2; }
const g = gen();
g.next(); // { value: 1, done: false }
[...gen()]; // [1, 2]
const range = {
from: 1, to: 3,
*[Symbol.iterator]() { for (let i = this.from; i <= this.to; i++) yield i; },
};
[...range]; // [1, 2, 3]
const key = Symbol("key");JavaScript Iterators and Generators Summary
- An iterator has
next()returning{ value, done }; an iterable has[Symbol.iterator](). for...of, spread and destructuring work with any iterable.function*creates generators that pause at eachyield.- Generators are lazy, so they can represent infinite sequences.
- Symbols are unique values, used for keys that never clash and built-in hooks like
Symbol.iterator.
JavaScript Iterators and Generators Examples
Each example runs in the page. Edit the code and press Run to see what changes.
Driving an iterator by hand
const letters = new Set(["x", "y"]);
const it = letters[Symbol.iterator]();
console.log(it.next());
console.log(it.next());
console.log(it.next());
function* steps() {
console.log(" (start)");
yield "first";
console.log(" (resumed)");
yield "second";
console.log(" (finished)");
}
const g = steps();
console.log("Created, nothing ran yet");
console.log(g.next().value);
console.log(g.next().value);
console.log(g.next().done);Line-by-line explanation
- Line 2Asking a Set for its iterator.
- Lines 4 to 6Each
next()returns the next value, thendone: true. - Lines 8 to 14A generator that logs as it runs, so you can see where it pauses.
- Lines 16 to 17Calling
steps()runs none of the body. - Lines 18 to 20Each
next()runs only up to the nextyield.
Making your own object iterable
const playlist = {
name: "Road trip",
songs: ["Intro", "Highway", "Sunset", "Home"],
*[Symbol.iterator]() {
for (const [i, song] of this.songs.entries()) {
yield `${i + 1}. ${song}`;
}
},
};
for (const line of playlist) console.log(line);
const [first, second] = playlist;
console.log("First two:", first, "|", second);
console.log("Count:", [...playlist].length);Line-by-line explanation
- Line 4A generator method as
Symbol.iteratormakes the object iterable. - Lines 5 to 7Each song is handed out with its number.
- Line 11
for...ofnow works on the playlist. - Line 13So does array destructuring.
- Line 15And spread.
Lazy and infinite sequences
function* fibonacci() {
let [a, b] = [0, 1];
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
function* take(iterable, n) {
if (n <= 0) return;
for (const value of iterable) {
yield value;
if (--n === 0) return;
}
}
console.log([...take(fibonacci(), 10)]);
const tag = Symbol("tag");
const item = { name: "Lamp", [tag]: "internal" };
console.log(Object.keys(item), item[tag], Symbol("a") === Symbol("a"));Line-by-line explanation
- Lines 3 to 6An endless sequence; each value is only calculated when requested.
- Lines 9 to 15A helper generator that stops after
nvalues. - Line 17Only the first ten numbers are ever calculated.
- Lines 19 to 21Symbol keys are hidden from
Object.keys, and every symbol is unique.
Common JavaScript Iterators and Generators Mistakes
Expecting a generator to run when called
function* setup() { console.log("ready"); yield; }
setup(); // logs nothingsetup().next(); // logs "ready"Spreading an infinite generator
const all = [...naturalNumbers()]; // never finishesconst firstTen = [...take(naturalNumbers(), 10)];Reusing a finished generator
const g = countTo(3);
[...g]; // [1, 2, 3]
[...g]; // []: the generator is already finished[...countTo(3)];
[...countTo(3)]; // call the generator function again for a fresh oneJavaScript Iterators and Generators in Real Projects
Where you'll meet these
Redux-Saga uses generators to describe step-by-step side effects. Database drivers and the Fetch API's streams use async iterators so you can process data in chunks with for await...of. Libraries mark private data with symbols to avoid clashing with your properties, and every built-in collection, from NodeLists to Maps, works with for...of through Symbol.iterator.
JavaScript Iterators and Generators Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
Hand out each value from the generator.
function* colours() {
"red";
"green";
}
console.log([...colours()]);
yield hands out a value and pauses.What is logged?
function* g() { yield 1; yield 2; return 3; }
const it = g();
console.log(it.next().value, [...it].length);What does an iterator's next() return?
Which key makes an object work with for...of?
range(start, end) should be a generator that yields every whole number from start to end inclusive, so [...range(2, 5)] is [2, 3, 4, 5]. Fix it.
function range(start, end) {
for (let i = start; i < end; i++) {
return i;
}
}
console.log([...range(2, 5)]);- Yields an inclusive range
- Single value range
- Is a generator
JavaScript Iterators and Generators Quiz
5 questions to check what you learned.
How do you declare a generator function?
- function gen()
- function* gen()
- generator gen()
- async gen()
Why can a generator use while (true) safely?
- It pauses at every yield and only continues when asked
- Browsers stop it after 1000 loops
- while (true) never runs in generators
- It runs in a separate thread
Are two symbols with the same description equal?
- Yes
- No, every symbol is unique
- Only in strict mode
- Only if they're global
Which of these is NOT iterable by default?
- A string
- A Map
- A generator object
- A plain object
What does yield* do?
- Hands out every value from another iterable in turn
- Yields a star character
- Ends the generator
- Doubles the value
JavaScript Iterators and Generators Challenge
A paginated list
Create an object pages with:
- An
itemsarray:["a", "b", "c", "d", "e"] - A
sizeof2 - A generator method
[Symbol.iterator]that yields the items in chunks ofsize, as arrays
So [...pages] should be [["a", "b"], ["c", "d"], ["e"]].
const pages = {
};
console.log([...pages]);- Yields chunks
- Respects the size
- Uses Symbol.iterator