Гид по технологиям

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

6 min read JavaScript Обновлено 15 Dec 2025
Итераторы и генераторы JavaScript
Итераторы и генераторы JavaScript

Большой логотип 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() могут быть заметны.
  • Для кратковременных операций с небольшими массивами использование генераторов усложнит код без выигрыша.

Шаблон: мини-методология при выборе

  1. Оцени размер данных и необходимость ленивой обработки.
  2. Если данные поступают как поток — рассмотрите Stream/async generator.
  3. Нужен ли контролируемый state/pausing? Если да — генератор подходит.
  4. Если нужна композиция и реактивность — RxJS/Observables.
  5. Напишите тесты на границы (пустые коллекции, один элемент, большие наборы).

Контроль качества: тесты и критерии приёмки

Критерии приёмки:

  • for…of проходит по всем ожидаемым элементам в правильном порядке.
  • При исчерпании итератора next() возвращает { value: undefined, done: true }.
  • Отсутствие утечек памяти при обработке больших коллекций (профайл памяти).
  • Пустые и вложенные коллекции обрабатываются корректно.

Минимальные тест-кейсы:

  • Пустая коллекция => итератор сразу done === true.
  • Коллекция из одного элемента => корректное значение и done.
  • Несколько последовательных вызовов next() возвращают ожидаемые значения.
  • Для объекта с Symbol.iterator: for…of не выбрасывает ошибок.

Чек-листы по ролям

Разработчик:

  • Добавил Symbol.iterator корректно и вернул объект с next().
  • Обработал пустые массивы и непредвиденные типы значений.
  • Написал unit-тесты на границы.

Техлид:

  • Проверил, что генераторы используются осмысленно и не усложняют код.
  • Оценил влияние на память и производительность.

Инженер по качеству:

  • Запустил тесты производительности на больших объёмах данных.
  • Проверил корректность встраивания в асинхронные цепочки (при необходимости).

Примеры распространённых паттернов

  1. Ленивый генератор, который читает элементы по требованию:
function* lazyRange(start = 0, end = Infinity) {
  let i = start;
  while (i <= end) {
    yield i++;
  }
}

// Использование:
for (const n of lazyRange(1, 5)) {
  console.log(n);
}
  1. Генерация уникальных строк-идентификаторов (простая):
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) исходя из модели данных и требований по памяти и параллелизму.

Спасибо за внимание — опробуйте генераторы на маленьких примерах и затем применяйте их там, где важна ленивость и контроль потока.

Поделиться: X/Twitter Facebook LinkedIn Telegram
Автор
Редакция

Похожие материалы

Отключить клавишу в Windows — быстро и просто
Windows

Отключить клавишу в Windows — быстро и просто

Line tone в Photoshop: эффект как на банкнотах
Графика

Line tone в Photoshop: эффект как на банкнотах

Slui 4 не работает в Windows — как исправить
Windows

Slui 4 не работает в Windows — как исправить

Исправить код ошибки 2 в Facebook
Социальные сети

Исправить код ошибки 2 в Facebook

Проверка состояния ПК в Windows 10 и 11
Компьютеры

Проверка состояния ПК в Windows 10 и 11

Создать армию клонов из фото — Photoshop и GIMP
Фотография

Создать армию клонов из фото — Photoshop и GIMP