Итераторы и генераторы в 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 } — конец
- 1 for…of вызывает Symbol.iteratorПолучает объект-итератор
- 2 Вызывает next()Получает { value, done }
- 3 done === false?Отдаёт value в тело цикла, снова next()
- 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.