Итераторы и генераторы
Цикл for...of перебирает массивы, строки, Map и Set. Он работает с ними одинаково, потому что все они следуют одному договору — протоколу итерации. В этом уроке разберём, как он устроен, и научимся создавать свои перебираемые объекты с помощью генераторов.
Перебираемые объекты
Перебираемым (iterable) называют объект, который можно обойти циклом for...of или разложить через .... Строка — один из них:
for (const letter of 'кот') {
console.log(letter);
}
// к
// о
// тОбычный объект {} перебираемым не является: for...of по нему выдаст ошибку.
Итератор и next
Внутри for...of работает итератор — объект с методом next(). Каждый вызов возвращает следующее значение в поле value и признак конца в поле done. Получить итератор можно вручную:
const letters = ['а', 'б'];
const iterator = letters[Symbol.iterator]();
const first = iterator.next();
console.log(first.value, first.done); // а false
console.log(iterator.next().value); // б
console.log(iterator.next().done); // trueЦикл for...of делает то же самое: вызывает next(), пока done не станет true.
Генераторы
Писать итераторы вручную долго. Функция-генератор, объявленная через function*, создаёт итератор сама. Слово yield отдаёт очередное значение:
function* count() {
yield 1;
yield 2;
yield 3;
}
for (const number of count()) {
console.log(number);
}
// 1
// 2
// 3Генератор ставит себя на паузу
Вызов генератора не выполняет его код, а возвращает итератор. Код идёт только до ближайшего yield и замирает до следующего next():
function* steps() {
console.log('Шаг 1');
yield;
console.log('Шаг 2');
}
const walker = steps();
walker.next(); // Шаг 1
console.log('Пауза'); // Пауза
walker.next(); // Шаг 2Бесконечные последовательности
Раз значения вычисляются по одному, генератор может быть бесконечным — например, выдавать новые номера заказов. Цикл while (true) здесь безопасен: после каждого yield генератор ждёт:
function* createIds() {
let id = 1;
while (true) {
yield id++;
}
}
const ids = createIds();
console.log(ids.next().value); // 1
console.log(ids.next().value); // 2Свой перебираемый объект
Объект становится перебираемым, если у него есть метод с особым именем [Symbol.iterator], который возвращает итератор. Проще всего сделать этот метод генератором:
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 4Частая ошибка
Перебирать один и тот же генератор дважды. Итератор одноразовый: дойдя до конца, он больше ничего не выдаёт:
function* count() {
yield 1;
yield 2;
yield 3;
}
const numbers = count();
console.log([...numbers].length); // 3
console.log([...numbers].length); // 0Если значения нужны повторно, вызовите генератор заново — count() — или сохраните результат в массив.