Encodera

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.

Оглавление

Итераторы и генераторы

Цикл 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() — или сохраните результат в массив.

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.