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

Что вы получите из этой статьи
- Понятие итератора и iterable-протокола в JavaScript; как устроен объект IteratorResult.
- Как вручную сделать обычный объект итерируемым с помощью Symbol.iterator.
- Чем генератор (function*) отличается от ручного итератора и как использовать yield.
- Практические шаблоны, альтернативы, ограничения и контроль качества.
Важно: кодовые примеры оставлены в оригинальном синтаксисе JavaScript. Комментарии и объяснения на русском.
Основные понятия
Итератор — это объект, который реализует протокол итератора: у него есть метод next(), возвращающий объект IteratorResult. IteratorResult содержит два свойства:
- done — булево: false, если есть следующее значение; true, когда последовательность закончилась.
- value — текущее значение; если done === true, value обычно undefined.
Iterable (итерируемый объект) — объект, который реализует метод Symbol.iterator и возвращает итератор. Благодаря этому его можно использовать в цикле for…of и других языковых конструкциях, принимающих итерируемые объекты.
Краткое определение: IteratorResult — объект { value, done }.
Простой пример: for…of
const fruits = ["Banana", "Mango", "Apple", "Grapes"];
for (const item of fruits) {
console.log(item);
}
// Выведет:
// Banana
// Mango
// Apple
// GrapesМассивы — встроенные iterable-объекты, поэтому for…of работает из коробки. Строки, Set и Map тоже итерируемы.
Что не является итерируемым по умолчанию
Обычные объекты (Object) не итерируемы. Попытка использовать for…of на объекте завершится ошибкой TypeError.
const iterObject = {
cars: ["Tesla", "BMW", "Toyota"],
animals: ["Cat", "Dog", "Hamster"],
food: ["Burgers", "Pizza", "Pasta"],
};
for (const item of iterObject) {
console.log(item);
}
// TypeError: iterObject is not iterableКак сделать объект итерируемым: Symbol.iterator
Чтобы объект стал iterable, нужно добавить ему метод Symbol.iterator, который возвращает объект-итератор с методом next(). Ниже показана пошаговая реализация, пояснения после.
iterObject[Symbol.iterator] = function () {
const objProperties = Object.keys(this);
let propertyIndex = 0;
let childIndex = 0;
return {
next: () => {
// Обработка ситуации, когда мы прошли все свойства
if (propertyIndex > objProperties.length - 1) {
return {
value: undefined,
done: true,
};
}
// Доступ к текущему массиву и текущему элементу
const properties = this[objProperties[propertyIndex]];
const property = properties[childIndex];
// Логика инкремента индексов
if (childIndex >= properties.length - 1) {
// если элементов в текущем массиве больше нет — перейти к следующему
childIndex = 0;
propertyIndex++;
} else {
// иначе перейти к следующему элементу текущего массива
childIndex++;
}
return {
done: false,
value: property,
};
},
};
};После этого for…of на iterObject будет работать: объект возвращает последовательные значения из всех вложенных массивов.
Примечание: ручная реализация итераторов быстро усложняется: легко допустить ошибку с индексацией, забыть обработать пустые массивы или потерять this-контекст. Поэтому вручную писать итераторы редко рекомендуется для сложных случаев.
Генераторы: удобный синтаксис для итераций
Генератор — это функция, выполнение которой можно приостанавливать и возобновлять. Генераторы возвращают объект Generator, у которого есть метод next(). Для объявления функции-генератора используют синтаксическую конструкцию function*.
function* example() {
return "Generator"
}
const gen = example();
console.log(gen.next()); // { value: 'Generator', done: true }В примере return завершает генератор и возвращает значение. Чаще генераторы используют с yield, который отдаёт значение и приостанавливает выполнение.
yield: приостановка и возобновление
function* example() {
yield "Model S"
yield "Model X"
yield "Cyber Truck"
return "Tesla"
}
const gen = example();
console.log(gen.next()); // { value: 'Model S', done: false }
console.log(gen.next()); // { value: 'Model X', done: false }
console.log(gen.next()); // { value: 'Cyber Truck', done: false }
console.log(gen.next()); // { value: 'Tesla', done: true }
console.log(gen.next()); // { value: undefined, done: true }Генератор можно также пройти через for…of — в этом цикле будут видны только значения до return (return не включается в итерацию):
for (const item of example()) {
console.log(item);
}
// Выведет:
// Model S
// Model X
// Cyber TruckПрактические сценарии применения
- Ленивые вычисления над большими коллекциями (постраничная загрузка, стримы).
- Бесконечные/псевдо-бесконечные последовательности (генерация уникальных ID, номера заказов).
- Построение pipeline обработки данных: генераторы помогают организовать шаги без накопления полного массива в памяти.
- Асинхронная обработка (async generators) для чтения потоков, API-стримов, WebSocket-сообщений.
Альтернативные подходы и когда они лучше
- Map/Set/Array methods (map, filter, reduce) — удобны для небольших коллекций и декларативной обработки. Но они создают новые промежуточные коллекции, что может увеличить память.
- Streams (Node.js streams, Web Streams API) — лучше для обработки реальных потоков данных и для интеграции с I/O; поддерживают backpressure.
- Reactive-подходы (RxJS) — подходят, когда нужно богатое управление временем, комбинирование потоков и сложные трансформации.
Используйте генераторы, когда нужна явная последовательная ленивость и контроль состояния между шагами. Выбирайте Stream/RxJS для I/O и асинхронных цепочек с управлением скоростью.
Примеры ошибок и когда итераторы/генераторы не подходят
- Когда требуется параллельная обработка больших массивов с высоким throughput — генераторы по умолчанию последовательны и не дают параллелизма.
- Если логика итерации критична к производительности на уровне C-подобного цикла, накладные расходы на вызовы next() могут быть заметны.
- Для кратковременных операций с небольшими массивами использование генераторов усложнит код без выигрыша.
Шаблон: мини-методология при выборе
- Оцени размер данных и необходимость ленивой обработки.
- Если данные поступают как поток — рассмотрите Stream/async generator.
- Нужен ли контролируемый state/pausing? Если да — генератор подходит.
- Если нужна композиция и реактивность — RxJS/Observables.
- Напишите тесты на границы (пустые коллекции, один элемент, большие наборы).
Контроль качества: тесты и критерии приёмки
Критерии приёмки:
- for…of проходит по всем ожидаемым элементам в правильном порядке.
- При исчерпании итератора next() возвращает { value: undefined, done: true }.
- Отсутствие утечек памяти при обработке больших коллекций (профайл памяти).
- Пустые и вложенные коллекции обрабатываются корректно.
Минимальные тест-кейсы:
- Пустая коллекция => итератор сразу done === true.
- Коллекция из одного элемента => корректное значение и done.
- Несколько последовательных вызовов next() возвращают ожидаемые значения.
- Для объекта с Symbol.iterator: for…of не выбрасывает ошибок.
Чек-листы по ролям
Разработчик:
- Добавил Symbol.iterator корректно и вернул объект с next().
- Обработал пустые массивы и непредвиденные типы значений.
- Написал unit-тесты на границы.
Техлид:
- Проверил, что генераторы используются осмысленно и не усложняют код.
- Оценил влияние на память и производительность.
Инженер по качеству:
- Запустил тесты производительности на больших объёмах данных.
- Проверил корректность встраивания в асинхронные цепочки (при необходимости).
Примеры распространённых паттернов
- Ленивый генератор, который читает элементы по требованию:
function* lazyRange(start = 0, end = Infinity) {
let i = start;
while (i <= end) {
yield i++;
}
}
// Использование:
for (const n of lazyRange(1, 5)) {
console.log(n);
}- Генерация уникальных строк-идентификаторов (простая):
function* idGenerator(prefix = 'id') {
let i = 1;
while (true) {
yield `${prefix}-${i++}`;
}
}
const ids = idGenerator('order');
console.log(ids.next().value); // order-1
console.log(ids.next().value); // order-2Паттерн миграции: от массивов к генераторам
- Найдите места, где вы создаёте промежуточные массивы (map/filter/flatMap).
- Оцените объём данных и память, потребляемую этими промежуточными коллекциями.
- Реализуйте генераторный pipeline: замените map/filter на yield внутри генератора.
- Запустите профилирование памяти и тесты производительности.
Безопасность и приватность
Генераторы и итераторы сами по себе не создают новых уязвимостей. Тем не менее:
- Не включайте в yield чувствительные данные без шифрования/маскировки.
- При сериализации и передачи значений убедитесь в контроле типов и длины данных.
- В асинхронных генераторах проверяйте ошибки источника данных и обрабатывайте исключения.
Советы и эвристики
- Если нужно только одноразово пройти по коллекции — обычные методы массива проще.
- Генераторы полезны, когда результат должен быть ленивым или потенциально бесконечным.
- Для асинхронных потоков используйте async function* и for await…of.
Краткий глоссарий (1 строка)
- Iterator: объект с методом next().
- Iterable: объект с методом Symbol.iterator, возвращающим итератор.
- Generator: функция* возвращающая объект Generator с next() и поддержкой yield.
- Yield: оператор, отдающий значение и приостанавливающий выполнение.
Быстрая шпаргалка (cheat sheet)
- Сделать объект iterable: obj[Symbol.iterator] = function() { return { next() { … } } }
- Объявить генератор: function* name() { yield 1; }
- Получить значение: const g = name(); g.next()
- Итерация по async-генератору: for await (const v of asyncGen()) { }
Когда итераторы/генераторы не работают: примеры
- Когда нужно массово распараллеливать вычисления по CPU — генератор не даст параллели.
- Когда проводят агрегации, требующие случайного доступа к данным — генератор обеспечивает последовательный доступ.
Заключение
Итераторы и генераторы — мощный инструмент для управления последовательностями в JavaScript. Генераторы особенно полезны, когда нужна ленивость, состояние между шагами и простота реализации итератора. Они не заменяют стримы и реактивные библиотеки, но в ряде задач дают чистое и понятное решение.
Основные выводы
- Генераторы упрощают реализацию итераторов и сокращают вероятность ошибок.
- Symbol.iterator делает любой объект совместимым с for…of.
- Выбирайте подход (массивы, генераторы, стримы, RxJS) исходя из модели данных и требований по памяти и параллелизму.
Спасибо за внимание — опробуйте генераторы на маленьких примерах и затем применяйте их там, где важна ленивость и контроль потока.
Похожие материалы
Отключить клавишу в Windows — быстро и просто
Line tone в Photoshop: эффект как на банкнотах
Slui 4 не работает в Windows — как исправить
Исправить код ошибки 2 в Facebook
Проверка состояния ПК в Windows 10 и 11