Итераторы и генераторы в JavaScript простыми словами

Содержание

О проверке кода. Примеры не запускались — в отличие от материалов по Node.js с фактическим выводом. Говорим прямо, а не ставим бейдж «проверено».

Итераторы и генераторы объясняют, как работает привычный for...of и открывают ленивые последовательности. Тема кажется абстрактной, но лежит в основе многих вещей JavaScript. Разберём без мистики.

Что делает объект перебираемым

Почему массив работает в for...of, а обычный объект — нет?

for (const x of [1, 2, 3]) {}       // ✅ массив перебирается
for (const x of 'abc') {}           // ✅ строка тоже
for (const x of { a: 1 }) {}        // ❌ TypeError: не итерируемый

Объект перебираем, если у него есть метод Symbol.iterator. У массивов, строк, Map, Set он встроен — поэтому они работают в for...of и spread. У обычного объекта его нет (для него используют Object.entries). Это протокол: for...of вызывает Symbol.iterator, получает итератор и крутит его.

Протокол итератора

Итератор — объект с методом next(), возвращающим { value, done }:

const iterator = [10, 20][Symbol.iterator]();

iterator.next();   // { value: 10, done: false }
iterator.next();   // { value: 20, done: false }
iterator.next();   // { value: undefined, done: true } — конец
Как for...of использует итератор
  1. 1 for…of вызывает Symbol.iteratorПолучает объект-итератор
  2. 2 Вызывает next()Получает { value, done }
  3. 3 done === false?Отдаёт value в тело цикла, снова next()
  4. 4 done === trueЦикл завершается

for...of под капотом просто крутит next(), пока done не станет true. Каждый вызов отдаёт очередное значение. Знание этого протокола позволяет сделать итерируемым свой объект — но вручную писать next() неудобно, поэтому есть генераторы.

Генераторы: итератор без ручной работы

Генератор — функция (function*), которая умеет приостанавливаться на yield и продолжать с того же места:

function* count() {
  yield 1;          // отдать 1 и заморозиться
  yield 2;          // при следующем next — отдать 2
  yield 3;
}

const gen = count();
gen.next();         // { value: 1, done: false }
gen.next();         // { value: 2, done: false }

// но обычно просто перебирают
for (const n of count()) {
  console.log(n);   // 1, 2, 3
}
[...count()];       // [1, 2, 3] — генератор итерируем автоматически

Генератор сам создаёт итератор — не нужно писать Symbol.iterator и next руками. Вызов генератора не выполняет тело сразу, а возвращает объект-итератор. Каждый next() выполняет код до следующего yield, отдаёт значение и замораживает функцию — с сохранением всех локальных переменных.

yield: пауза с памятью

function* idGenerator() {
  let id = 1;
  while (true) {           // бесконечный цикл — но это безопасно!
    yield id++;            // отдаём и ждём следующего запроса
  }
}

const ids = idGenerator();
ids.next().value;   // 1
ids.next().value;   // 2 — переменная id сохранилась между вызовами!
ids.next().value;   // 3

yield отдаёт значение и приостанавливает функцию до следующего next(). При возобновлении выполнение продолжается сразу после yield, а локальные переменные (id) сохраняются — как в замыкании. Поэтому бесконечный while (true) здесь безопасен: тело выполняется не всё сразу, а по одному шагу на запрос. Обычный цикл завис бы, генератор — нет.

Зачем это на практике

Главная ценность генераторов — ленивые последовательности, где значения вычисляются по требованию:

  • бесконечные последовательности — генератор ID, случайных чисел, чисел Фибоначчи: производим ровно столько, сколько запросили;
  • чтение больших данных по кускамчитать файл построчно, не загружая целиком в память;
  • свои итерируемые структуры — сделать дерево или связный список перебираемым в for...of парой строк;
  • асинхронные генераторы (async function* + for await) — перебор потоков данных, приходящих со временем.
// диапазон чисел — ленивый, без создания массива
function* range(start, end) {
  for (let i = start; i <= end; i++) yield i;
}
[...range(1, 5)];   // [1, 2, 3, 4, 5]

Ключевая идея — не создавать всю последовательность заранее, а выдавать элементы по мере надобности. Для миллиона элементов это разница между забитой памятью и лёгким генератором.


Смежные темы: Symbol — Symbol.iterator под капотом; замыкания — как хранится состояние; spread и rest — работают с итерируемыми. Полный список — в уроках JavaScript.

Частые вопросы

Что такое итератор в JavaScript?
Объект с методом next, который при каждом вызове возвращает следующее значение в формате { value, done }. Когда done становится true, перебор закончен. На протоколе итератора построен цикл for…of.
Что делает объект перебираемым в for...of?
Наличие метода Symbol.iterator, который возвращает итератор. Массивы, строки, Map и Set уже его имеют, поэтому работают в for…of. Обычный объект его не имеет, поэтому в for…of напрямую не перебирается.
Что такое генератор?
Функция, объявленная через function*, которая может приостанавливать выполнение на слове yield и возобновлять с того же места. Она автоматически создаёт итератор, поэтому генератор — простой способ сделать перебираемую последовательность.
Что делает yield?
Ключевое слово внутри генератора, которое отдаёт значение наружу и приостанавливает функцию до следующего вызова next. При возобновлении выполнение продолжается сразу после yield, сохраняя все локальные переменные.
Зачем нужны генераторы на практике?
Для ленивых и бесконечных последовательностей, которые вычисляются по требованию, а не заранее. Например, генератор ID или чтение большого файла по кускам без загрузки целиком. Значения производятся ровно тогда, когда их запрашивают.