JavaScript course menu
JavaScript Iterators and Generators: Symbol.iterator, function* and yield
Learn the JavaScript iteration protocol: iterables and iterators, Symbol.iterator, making your own objects work with for...of and spread, generator functions with function* and yield, lazy and infinite sequences, and async iteration with for await...of.
On this page
for...of, spread (...), destructuring and Array.from all work on arrays, strings, Maps and Sets. They work because those types follow the iteration protocol. You can make your own objects follow it too, most easily with generators.
Iterables and iterators#
- An iterable is an object with a
[Symbol.iterator]()method that returns an iterator. - An iterator is an object with a
next()method that returns{ value, done }each time it is called.
You can drive an iterator by hand to see how for...of works:
const letters = ["a", "b"];
const iterator = letters[Symbol.iterator]();
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());{ value: 'a', done: false }
{ value: 'b', done: false }
{ value: undefined, done: true }for...of simply calls next() until done is true.
Making an object iterable#
const range = {
from: 1,
to: 4,
[Symbol.iterator]() {
let current = this.from;
const last = this.to;
return {
next() {
return current <= last ? { value: current++, done: false } : { value: undefined, done: true };
},
};
},
};
for (const n of range) console.log(n);
console.log([...range]);
console.log(Math.max(...range));1
2
3
4
[ 1, 2, 3, 4 ]
4Now range works everywhere an iterable is expected. Writing iterators by hand is verbose, though; generators make it much shorter.
Generators#
A generator function is written with function*. Calling it returns a generator (an iterator) without running the body. Each next() runs the body until the next yield, which hands out a value and pauses:
function* countdown(from) {
console.log("(starting)");
while (from > 0) {
yield from;
from--;
}
return "lift off";
}
const gen = countdown(3);
console.log(gen.next());
console.log(gen.next());
console.log([...gen]);
console.log(gen.next());(starting)
{ value: 3, done: false }
{ value: 2, done: false }
[ 1 ]
{ value: undefined, done: true }- Nothing ran until the first
next(): notice when "(starting)" appears. - Spread used up the remaining value; the
returnvalue is not included in iteration. - A generator can only be iterated once.
The range object from earlier, rewritten with a generator method:
const range = {
from: 1,
to: 4,
*[Symbol.iterator]() {
for (let n = this.from; n <= this.to; n++) yield n;
},
};
console.log([...range]);[ 1, 2, 3, 4 ]Lazy and infinite sequences#
Generators produce values on demand, so a sequence can even be infinite, as long as the consumer stops:
function* ids(prefix) {
let n = 1;
while (true) yield `${prefix}-${String(n++).padStart(3, "0")}`;
}
const orderIds = ids("ORD");
console.log(orderIds.next().value);
console.log(orderIds.next().value);
function* take(iterable, count) {
for (const item of iterable) {
if (count-- <= 0) return;
yield item;
}
}
function* fibonacci() {
let [a, b] = [0, 1];
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
console.log([...take(fibonacci(), 10)]);ORD-001
ORD-002
[
0, 1, 1, 2, 3,
5, 8, 13, 21, 34
](Node.js prints longer arrays of numbers across several lines like this.) Nothing is computed until it is needed, which also helps when processing large data piece by piece.
yield*#
yield* delegates to another iterable, yielding each of its values:
function* allCourses() {
yield* ["HTML", "CSS"];
yield* "JS";
}
console.log([...allCourses()]);[ 'HTML', 'CSS', 'J', 'S' ]Async iteration#
for await...of loops over async iterables, waiting for each value. Async generators (async function*) produce them, for example when reading data page by page from an API:
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function* fetchPages(total) {
for (let page = 1; page <= total; page++) {
await wait(10); // stand-in for a network request
yield [`item ${page}a`, `item ${page}b`];
}
}
for await (const items of fetchPages(3)) {
console.log(items);
}[ 'item 1a', 'item 1b' ]
[ 'item 2a', 'item 2b' ]
[ 'item 3a', 'item 3b' ]Node.js streams, such as reading a file line by line, are async iterables too.
Practice#
Practice exercise
Write a generator evens(limit) that yields the even numbers from 0 up to and including limit, and a generator map(iterable, fn) that yields fn(item) for each item. Use them together to print the squares of the even numbers up to 10.
Show solution
function* evens(limit) {
for (let n = 0; n <= limit; n += 2) yield n;
}
function* map(iterable, fn) {
for (const item of iterable) yield fn(item);
}
console.log([...map(evens(10), (n) => n * n)]);[ 0, 4, 16, 36, 64, 100 ]Key takeaway#
Finished this lesson?