Итераторы и генераторы в JavaScript: как и когда использовать
Введение
Перебор коллекций традиционными циклами быстро становится неудобным и медленным при работе с огромными объёмами данных. JavaScript-итераторы и генераторы решают эту проблему: они позволяют контролировать поток итерации, возвращать значения по одному и приостанавливать/возобновлять выполнение.
В этой статье подробно рассмотрены принципы работы итераторов, пример ручной реализации итератора на объекте и эквивалентное решение с помощью генератора. Также приведены рекомендации по применению, ограничения и контроль качества.
Основы: что такое итератор
Итератор — это объект, который реализует протокол итератора. Такой объект содержит метод next. Этот метод возвращает объект, соответствующий интерфейсу IteratorResult.
Интерфейс IteratorResult содержит два свойства: done и value. Свойство done — булево: false, если итератор может вернуть следующее значение, и true, когда последовательность завершена. Свойство value — возвращаемое значение; при завершении итерации (когда done === true) value обычно равно undefined.
Итераторы позволяют “перебирать” объекты JavaScript, такие как массивы или Map. Это работает благодаря протоколу Iterable.
В JavaScript протокол iterable — стандарт, задающий объекты, по которым можно итерироваться (например, в цикле for…of).
Пример простого перебора массива:
const fruits = ["Banana", "Mango", "Apple", "Grapes"];
for (const iterator of fruits) {
console.log(iterator);
}
/*
Banana
Mango
Apple
Grapes
*/
В этом примере цикл for…of вызывает внутренний итератор массива и выводит элементы по одному.
Некоторые типы в JavaScript (Array, String, Set, Map) — встроенные iterable-объекты, потому что они или их прототипы реализуют метод @@iterator (в коде используется Symbol.iterator).
Объекты типа plain Object по умолчанию не итерируемы. Попытка сделать for…of over plain object приведёт к ошибке:
const iterObject = {
cars: ["Tesla", "BMW", "Toyota"],
animals: ["Cat", "Dog", "Hamster"],
food: ["Burgers", "Pizza", "Pasta"],
};
for (const iterator of iterObject) {
console.log(iterator);
}
// TypeError: iterObject is not iterable
Как сделать объект итерируемым
Чтобы сделать объект итерируемым, нужно реализовать на нём метод Symbol.iterator. Этот метод должен возвращать объект с методом next(), который возвращает { value, done }.
Symbol.iterator в спецификации соответствует обозначению @@iterator — в коде используется Symbol.iterator.
Ниже пошаговая демонстрация подхода, использованного для iterObject.
Сначала добавим метод Symbol.iterator с декларацией функции:
iterObject[Symbol.iterator] = function () {
// Subsequent code blocks go here...
}
Затем нужно получить список ключей объекта. Это можно сделать через Object.keys(this):
let objProperties = Object.keys(this)
Доступ к этому массиву позволит вам задать поведение итерации.
Далее объявим счётчики, которые будут отслеживать позицию по свойствам и в дочерних массивах:
let propertyIndex = 0;
let childIndex = 0;
Теперь нужно реализовать и вернуть метод next(). Он должен корректно обрабатывать случай, когда объект полностью перебран — тогда возвращается { value: undefined, done: true }.
return {
next() {
// Subsequent code blocks go here...
}
}
Обработаем крайний случай, когда propertyIndex превышает количество свойств:
if (propertyIndex > objProperties.length - 1) {
return {
value: undefined,
done: true,
};
}
Затем получаем текущие родительское свойство и дочерний элемент:
// Accessing parent and child properties
const properties = this[objProperties[propertyIndex]];
const property = properties[childIndex];
И реализуем логику инкремента счётчиков: если дочерний массив исчерпан — сбрасываем childIndex и переходим к следующему свойству; иначе — просто увеличиваем childIndex.
// Index incrementing logic
if (childIndex >= properties.length - 1) {
// if there are no more elements in the child array
// reset child index
childIndex = 0;
// Move to the next property
propertyIndex++;
} else {
// Move to the next element in the child array
childIndex++
}
И в конце возвращаем текущий элемент с done: false:
return {
done: false,
value: property,
};
Полная реализация Symbol.iterator, описанная выше, выглядит так:
iterObject[Symbol.iterator] = function () {
const objProperties = Object.keys(this);
let propertyIndex = 0;
let childIndex = 0;
return {
next: () => {
//Handling edge case
if (propertyIndex > objProperties.length - 1) {
return {
value: undefined,
done: true,
};
}
// Accessing parent and child properties
const properties = this[objProperties[propertyIndex]];
const property = properties[childIndex];
// Index incrementing logic
if (childIndex >= properties.length - 1) {
// if there are no more elements in the child array
// reset child index
childIndex = 0;
// Move to the next property
propertyIndex++;
} else {
// Move to the next element in the child array
childIndex++
}
return {
done: false,
value: property,
};
},
};
};
После этой реализации можно без ошибок запустить for…of по iterObject.
Важно: ручная реализация итератора, как показано выше, часто ненадёжна и подвержена ошибкам; проще и безопаснее использовать генераторы.
Генераторы в JavaScript
Генераторная функция — это функция, выполнение которой можно приостанавливать и возобновлять. Она возвращает последовательность значений во времени.
Генератор создаётся аналогично обычной функции, но после ключевого слова function ставится звёздочка (*).
function* example () {
return "Generator"
}
Вызов обычной функции возвращает значение через return или undefined. Генераторная функция при вызове не исполняет тело сразу, а возвращает объект Generator. Чтобы получить текущее значение, надо вызвать метод next() у этого объекта.
const gen = example();
console.log(gen.next()); // { value: 'Generator', done: true }
В примере выше значение value пришло из return, что завершило генератор. Обычно при работе с генераторами вместо return используются yield, потому что yield позволяет возвращать несколько значений и приостанавливать выполнение.
Ключевое слово yield
yield приостанавливает выполнение генераторной функции и возвращает указанное значение. При следующем вызове next() выполнение возобновляется после 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 }
Вызывая next() несколько раз, вы увидите, как генератор постепенно выдаёт значения:
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 не попадёт в цикл (for…of итерирует только значения, возвращаемые через yield).
for (const iterator of gen) {
console.log(iterator);
}
/*
Model S
Model X
Cyber Truck
*/
Практическое применение: когда использовать итераторы и генераторы
- Перебор больших коллекций по частям, чтобы уменьшить пиковую нагрузку на память.
- Потоковые или ленивые вычисления: вычислять элементы только по требованию.
- Бесконечные последовательности (например, уникальные идентификаторы или генерация временных меток).
- Сложные сценарии обхода пользовательских структур данных (графы, деревья) с кастомной логикой перехода.
Генераторы значительно упрощают код по сравнению с ручной реализацией next(). Они лаконичны и легче читаются.
Когда это не подходит
- Если данные уже загружены в память и простые методы массива (map, forEach) проще и быстрее в реализации — не стоит усложнять код итераторами.
- Для простых локальных преобразований коллекций (один проход, без ленивой загрузки) удобнее цепочки методов Array.
- Если требуется асинхронное получение значений (например, чтение из сети), используйте async-итераторы и async генераторы (function -> async function), а не синхронные генераторы.
Контрпример: если вам нужно мгновенно преобразовать небольшой массив и получить новый массив на выходе — Array.prototype.map будет короче и понятнее.
Альтернативные подходы
- Методы массива: map, reduce, filter, forEach — для полноразмерных синхронных преобразований.
- Streams (например, Node.js streams) — для работы с потоками данных на диске или по сети.
- Async Iterators (async function*) — для асинхронных источников данных.
- Внешние библиотеки для ленивых вычислений (например, lazy.js) — когда требуется богатая функциональность и цепочки трансформаций.
Эвристики и ментальные модели
- Ментальная модель “ленивости”: итераторы и генераторы возвращают значение только когда оно нужно.
- “Поставщик значений”: представьте генератор как функцию-поставщика, которая отдает по одному значению и запоминает своё состояние.
- “Стек приостановок”: каждый yield запоминает точку возобновления, как маркер в коде.
Эвристика выбора: если вам нужно контролировать поток данных, уменьшить память или реализовать бесконечную последовательность — выбирайте генераторы.
Примеры и советы по использованию
- Чтобы преобразовать итератор в массив: Array.from(iterator) или […iterator]. Это полезно для тестирования и отладки.
- Не сохраняйте внешнее состояние в генераторе без надобности. Предпочитайте локальное состояние внутри функции-генератора.
- Для асинхронных потоков используйте for await…of с async генераторами.
Пример генератора с параметром и бесконечной последовательностью простых id:
function* idGenerator(prefix = "id") {
let i = 0;
while (true) {
yield `${prefix}-${i++}`;
}
}
const ids = idGenerator("user");
console.log(ids.next()); // {value: 'user-0', done: false}
console.log(ids.next()); // {value: 'user-1', done: false}
Сравнение: ручной итератор vs генератор
- Плюсы ручного итератора: полная контрольируемость формата итератора и строгая оптимизация для специфичных кейсов.
- Минусы ручного итератора: больше шаблонного кода, шанс на ошибки при управлении состоянием.
- Плюсы генератора: лаконичность, удобство чтения, встроенный механизм приостановки.
- Минусы генератора: иногда менее очевиден поток состояний при сложной логике, и генераторы нельзя сериализовать напрямую.
Критерии приёмки
- Код корректно итерирует все ожидаемые элементы без дублирования.
- После завершения итерации next() возвращает { value: undefined, done: true }.
- Нет утечек памяти: генератор не удерживает ссылок на большие объекты после завершения.
- Для асинхронных операций используется async итератор и for await…of.
- Юнит-тесты покрывают граничные случаи (пустая коллекция, одноэлементная коллекция, множественные итерации).
Чек-листы по ролям
Developer:
- Использовать генератор, если требуется ленивый перебор.
- Не хранить глобальное состояние в итераторе без нужды.
- Добавить тесты на граничные сценарии.
Code reviewer:
- Проверить, что next() корректно обрабатывает завершение.
- Проверить отсутствие побочных эффектов при повторном вызове итератора.
- Проверить читаемость и документацию генератора.
Набор тестов и критерии приёмки
- Тест 1: итерация по пустому источнику должна завершаться сразу (done === true).
- Тест 2: итерация по источнику с одним элементом должна вернуть этот элемент, затем done === true.
- Тест 3: повторный вызов Symbol.iterator на объекте должен начинать новую независимую итерацию (если это ожидаемое поведение).
- Тест 4: генератор бесконечной последовательности не должен бросать исключений при многократных вызовах next(), и выдавать уникальные значения.
Краткий глоссарий
- Iterator: объект с методом next(), возвращающим { value, done }.
- Iterable: объект с методом Symbol.iterator, который возвращает итератор.
- Generator: функция с “*” и yield, автоматически создающая итератор.
- yield: оператор, приостанавливающий генератор и отдающий значение.
Резюме
Итераторы и генераторы — мощный инструмент для работы с большими и ленивыми потоками данных. Генераторы обычно предпочтительнее из-за простоты и читаемости, но ручные итераторы всё ещё имеют место при необходимости очень специфического поведения. Всегда выбирайте инструмент, исходя из требований по памяти, асинхронности и читаемости кода.
Важно: при работе с асинхронными источниками данных используйте async генераторы и for await…of.
Краткое резюме ключевых практик и советов представлено выше.
Похожие материалы
RDP: полный гид по настройке и безопасности
Android как клавиатура и трекпад для Windows
Советы и приёмы для работы с PDF
Calibration в Lightroom Classic: как и когда использовать
Отключить Siri Suggestions на iPhone