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

Протокол итератора, Symbol.iterator, function* и ленивые бесконечные последовательности.

Итератор — объект с методом next(), возвращающим { value, done }. Итерируемый объект имеет метод [Symbol.iterator].

Протокол итерации вручную

Конструкции for...of и spread (...) работают с любым объектом, у которого есть [Symbol.iterator]. Реализуем «диапазон» с нуля:

const range = {
  from: 1, to: 5,
  [Symbol.iterator]() {
    let current = this.from;
    const last = this.to;
    return {
      next() {
        if (current <= last) return { value: current++, done: false };
        return { value: undefined, done: true };
      }
    };
  }
};

console.log([...range].join(", "));
console.log("сумма:", [...range].reduce((a, b) => a + b, 0));

Вывод:

1, 2, 3, 4, 5
сумма: 15

function* — генераторы упрощают всё

Генератор автоматически реализует протокол итератора. Каждый yield «ставит на паузу» функцию и отдаёт значение; next() возобновляет её:

function* idGenerator() {
  let id = 1;
  while (true) {
    yield id++;
  }
}

const gen = idGenerator();
console.log(gen.next().value);
console.log(gen.next().value);
console.log(gen.next().value);

Вывод:

1
2
3

Ленивость: бесконечная последовательность

Генератор вычисляет значения по требованию. Бесконечный while (true) безопасен, потому что значения берутся ровно по запросу. Возьмём первые 5 натуральных чисел из бесконечного потока:

function* naturals() {
  let n = 1;
  while (true) yield n++;
}

function take(iterable, count) {
  const result = [];
  for (const x of iterable) {
    if (result.length >= count) break;
    result.push(x);
  }
  return result;
}

console.log(take(naturals(), 5).join(", "));

Вывод:

1, 2, 3, 4, 5

Двусторонний обмен и yield*

Генераторы умеют не только отдавать, но и принимать значения через аргумент next(value). А yield* делегирует другому генератору:

function* dialog() {
  const name = yield "Как тебя зовут?";
  const age = yield "Привет, " + name + "! Сколько тебе лет?";
  return name + ", возраст " + age;
}

const g = dialog();
console.log(g.next().value);
console.log(g.next("Аня").value);
console.log(g.next(25).value);

Вывод:

Как тебя зовут?
Привет, Аня! Сколько тебе лет?
Аня, возраст 25

Значение, переданное в next("Аня"), стало результатом первого yield. Так строится двусторонний диалог с генератором.

function* inner() { yield "a"; yield "b"; }
function* outer() {
  yield 1;
  yield* inner();  // делегируем все yield из inner
  yield 2;
}
console.log([...outer()].join(", "));

Вывод:

1, a, b, 2

Итог

  • Итерируемость даёт метод [Symbol.iterator]; его использует for...of и spread.
  • function* автоматически реализует протокол; yield ставит на паузу.
  • Генераторы ленивы — годятся для бесконечных потоков; yield* делегирует.
Проверьте себя
1. Какой метод делает объект перебираемым в for...of?
Anext()
B[Symbol.iterator]()
CforEach()
D[Symbol.asyncIterator]()
2. Почему бесконечный генератор с while(true) безопасен?
AОн автоматически останавливается
BЗначения вычисляются лениво, по одному на каждый next()
CБраузер ограничивает циклы
DОн на самом деле конечен
3. Что делает yield* внутри генератора?
AВозвращает массив
BДелегирует перебор другому генератору/итерируемому
CОстанавливает генератор
DУмножает значение
Поддержать проект