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

Итераторы и генераторы в JavaScript: как и когда использовать

7 min read JavaScript Обновлено 09 Jan 2026
Итераторы и генераторы JavaScript — практическое руководство
Итераторы и генераторы JavaScript — практическое руководство

Крупный логотип 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.


Краткое резюме ключевых практик и советов представлено выше.

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

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

RDP: полный гид по настройке и безопасности
Инфраструктура

RDP: полный гид по настройке и безопасности

Android как клавиатура и трекпад для Windows
Гайды

Android как клавиатура и трекпад для Windows

Советы и приёмы для работы с PDF
Документы

Советы и приёмы для работы с PDF

Calibration в Lightroom Classic: как и когда использовать
Фото

Calibration в Lightroom Classic: как и когда использовать

Отключить Siri Suggestions на iPhone
iOS

Отключить Siri Suggestions на iPhone

Рисование таблиц в Microsoft Word — руководство
Office

Рисование таблиц в Microsoft Word — руководство