Iterators and generators
The for...of loop goes over arrays, strings, Map and Set. It treats them all the same way because they all follow one agreement — the iteration protocol. In this lesson we look at how it works and learn to create our own iterable objects with generators.
Iterable objects
An iterable is an object that can be walked through with for...of or spread with .... A string is one of them:
for (const letter of 'cat') {
console.log(letter);
}
// c
// a
// tA plain object {} is not iterable: for...of over it throws an error.
The iterator and next
Inside for...of works an iterator — an object with a next() method. Each call returns the next value in the value field and an end flag in the done field. You can get an iterator by hand:
const letters = ['a', 'b'];
const iterator = letters[Symbol.iterator]();
const first = iterator.next();
console.log(first.value, first.done); // a false
console.log(iterator.next().value); // b
console.log(iterator.next().done); // trueThe for...of loop does the same: it calls next() until done becomes true.
Generators
Writing iterators by hand takes time. A generator function, declared with function*, creates an iterator by itself. The yield keyword hands out the next value:
function* count() {
yield 1;
yield 2;
yield 3;
}
for (const number of count()) {
console.log(number);
}
// 1
// 2
// 3A generator pauses itself
Calling a generator does not run its code; it returns an iterator. The code runs only up to the nearest yield and freezes until the next next():
function* steps() {
console.log('Step 1');
yield;
console.log('Step 2');
}
const walker = steps();
walker.next(); // Step 1
console.log('Pause'); // Pause
walker.next(); // Step 2Infinite sequences
Since values are computed one at a time, a generator can be endless — for example, handing out new order numbers. The while (true) loop is safe here: after each yield the generator waits:
function* createIds() {
let id = 1;
while (true) {
yield id++;
}
}
const ids = createIds();
console.log(ids.next().value); // 1
console.log(ids.next().value); // 2Your own iterable object
An object becomes iterable if it has a method with the special name [Symbol.iterator] that returns an iterator. The simplest way is to make that method a generator:
const range = {
from: 1,
to: 4,
*[Symbol.iterator]() {
for (let i = this.from; i <= this.to; i++) {
yield i;
}
},
};
console.log([...range].join(' ')); // 1 2 3 4Common mistake
Iterating the same generator twice. An iterator is single-use: once it reaches the end, it yields nothing more:
function* count() {
yield 1;
yield 2;
yield 3;
}
const numbers = count();
console.log([...numbers].length); // 3
console.log([...numbers].length); // 0If you need the values again, call the generator anew — count() — or save the result into an array.