⚡ JavaScript

Итераторы и генераторы в JavaScript (Node.js): подробное руководство

Что это такое и зачем? (аналогия из реальной жизни)

Представьте, что вы — библиотекарь, и перед вами стоит огромный стеллаж с тысячами книг. Вам нужно выписать названия всех книг, которые начинаются на букву «П». Как бы вы это сделали?

Первый способ (плохой): Вы берете все книги со стеллажа, сваливаете их в огромную кучу на полу, и только потом начинаете перебирать. Проблема: у вас закончится место, вы устанете таскать тяжести, а если книг миллион — вы просто не сможете их все удержать в голове или в комнате.

Второй способ (хороший): Вы подходите к стеллажу, берете первую книгу, смотрите на название, записываете, если нужно, и кладете её обратно. Потом берете вторую, третью... Вы не храните все книги у себя — вы просто «проходите» по ним по одной, делая с каждой нужное действие.

В программировании происходит то же самое. У нас есть данные: массивы, строки, списки, результаты запросов к базе данных. Нам часто нужно пройтись по всем элементам по порядку, что-то с ними сделать. Но хранить все данные в памяти сразу — дорого и часто невозможно.

Итератор — это как раз тот самый «библиотекарь», который умеет ходить по стеллажу и выдавать вам книги по одной. Он знает, где начало, где конец, и как перейти к следующему элементу.

Генератор — это «фабрика» по производству итераторов. Вы говорите генератору: «Мне нужны числа от 1 до бесконечности, но только четные». И генератор создает итератор, который будет выдавать вам эти числа по одному, не пытаясь создать бесконечный список в памяти.

💡 Важно: Итераторы и генераторы — это не про то, какие данные у вас есть, а про то, как вы к ним обращаетесь. Это инструменты для эффективного перебора.

Зачем это нужно в реальной жизни?

  • Экономия памяти: Представьте, что вы читаете файл размером 10 гигабайт. Если вы загрузите его целиком в память — программа упадет. Итератор позволяет читать по одной строке.
  • Работа с бесконечными последовательностями: Например, бесконечный поток данных с датчика температуры. Вы не можете создать массив «всех будущих температур».
  • Ленивые вычисления: Вычисляем значение только тогда, когда оно реально понадобилось. Это ускоряет программы.
  • Удобство чтения кода: Цикл for...of с итератором выглядит понятнее, чем ручное управление индексами.

Теория: как это работает изнутри

Протокол итератора (Iterator Protocol)

В JavaScript есть строгий «договор» — протокол, которому должен следовать любой объект, чтобы его можно было перебирать. Этот договор состоит из двух частей:

1. Объект должен иметь метод next()

Этот метод вызывается каждый раз, когда мы хотим получить следующий элемент. Он возвращает объект с двумя полями:

  • value — значение текущего элемента (может быть любого типа).
  • done — булево значение: false, если есть ещё элементы, и true, если перебор закончен.

2. Объект должен быть «итерируемым» (iterable)

Чтобы объект можно было использовать в цикле for...of или spread-операторе ([...obj]), у него должен быть метод Symbol.iterator. Этот метод возвращает сам итератор (объект с методом next).

🔍 Деталь: Symbol.iterator — это специальный символ, который не может случайно совпасть с именем обычного свойства. Он как секретный пароль, который говорит JavaScript: «Этот объект умеет перебираться».

Как это выглядит «под капотом»

Когда вы пишете:

for (let item of myArray) {
  console.log(item);
}

JavaScript делает примерно следующее:

// 1. Получает итератор из массива
let iterator = myArray[Symbol.iterator]();

// 2. Запускает бесконечный цикл
let result = iterator.next();
while (!result.done) {
  let item = result.value;   // текущий элемент
  console.log(item);         // тело вашего цикла
  result = iterator.next();  // переходим к следующему
}

То есть цикл for...of — это просто «синтаксический сахар» для ручного вызова next().

Генераторы: фабрика итераторов

Генератор — это функция, которая возвращает итератор. Но не простой, а «ленивый». Обычная функция выполняется от начала до конца и возвращает одно значение. Генератор может «заморозить» своё выполнение в любой точке, вернуть промежуточное значение, а потом продолжить с того же места.

Генератор обозначается звёздочкой после function:

function* myGenerator() {
  yield 1;
  yield 2;
  yield 3;
}

Ключевое слово yield — это как return, но с возможностью «вернуться». Когда генератор доходит до yield, он возвращает значение и останавливается. При следующем вызове next() он продолжает выполнение со следующей строки.

⚡ Аналогия: Представьте, что вы читаете книгу и ставите закладку на каждой интересной странице. yield — это закладка. Вы прочитали страницу, поставили закладку, закрыли книгу. Потом открыли — и читаете дальше с того же места.

Внутреннее устройство генератора

Когда вы вызываете функцию-генератор, она не выполняет свой код сразу. Вместо этого она возвращает объект-итератор (Generator Object). Этот объект имеет:

  • Метод next() — запускает выполнение до следующего yield.
  • Метод return() — завершает генератор досрочно.
  • Метод throw() — «бросает» ошибку внутрь генератора.

Каждый раз, когда вызывается next(), генератор «просыпается», выполняет код до следующего yield, «засыпает» и возвращает значение.

Синтаксис в JavaScript (Node.js): все формы и варианты

1. Создание итератора вручную

Вы можете сделать итерируемым любой объект, добавив ему метод Symbol.iterator:

// Создаём объект, который будет перебирать числа от start до end
const range = {
  start: 1,
  end: 5,
  
  // Главный метод — Symbol.iterator
  [Symbol.iterator]() {
    let current = this.start;
    const end = this.end;
    
    // Возвращаем объект-итератор с методом next()
    return {
      next() {
        if (current <= end) {
          return { value: current++, done: false };
        } else {
          return { done: true }; // value можно не указывать
        }
      }
    };
  }
};

// Теперь можно использовать for...of
for (let num of range) {
  console.log(num); // 1, 2, 3, 4, 5
}

2. Функция-генератор (базовый синтаксис)

// Объявление функции-генератора
function* generateSequence() {
  yield 1;
  yield 2;
  yield 3;
}

// Создаём итератор
const generator = generateSequence();

// Используем
console.log(generator.next()); // { value: 1, done: false }
console.log(generator.next()); // { value: 2, done: false }
console.log(generator.next()); // { value: 3, done: false }
console.log(generator.next()); // { value: undefined, done: true }

// Или в цикле
for (let value of generateSequence()) {
  console.log(value); // 1, 2, 3
}

3. Генератор с аргументами

function* generateRange(start, end, step = 1) {
  for (let i = start; i <= end; i += step) {
    yield i;
  }
}

// Использование
const evenNumbers = generateRange(0, 10, 2);
console.log([...evenNumbers]); // [0, 2, 4, 6, 8, 10]

4. Генератор как метод объекта

const myObj = {
  *generateIds() {
    let id = 1;
    while (true) {
      yield id++;
    }
  }
};

const idGenerator = myObj.generateIds();
console.log(idGenerator.next().value); // 1
console.log(idGenerator.next().value); // 2
console.log(idGenerator.next().value); // 3
// ... бесконечно

5. Композиция генераторов (yield*)

Можно вкладывать один генератор в другой с помощью yield*:

function* generateNumbers() {
  yield 1;
  yield 2;
}

function* generateLetters() {
  yield 'a';
  yield 'b';
}

function* generateAll() {
  yield* generateNumbers(); // делегируем перебор первому генератору
  yield* generateLetters(); // потом второму
  yield 'end';
}

console.log([...generateAll()]); // [1, 2, 'a', 'b', 'end']

6. Генератор с return и throw

function* myGenerator() {
  try {
    yield 1;
    yield 2;
    yield 3;
  } catch (error) {
    console.log('Поймали ошибку:', error.message);
  }
}

const gen = myGenerator();
console.log(gen.next()); // { value: 1, done: false }

// Досрочное завершение
console.log(gen.return('завершено досрочно')); 
// { value: 'завершено досрочно', done: true }

// Или можно "бросить" ошибку внутрь
const gen2 = myGenerator();
gen2.next(); // { value: 1, done: false }
gen2.throw(new Error('Что-то пошло не так')); 
// Выведет: "Поймали ошибку: Что-то пошло не так"

7. Асинхронные генераторы (async generators)

Для работы с потоками данных, которые приходят не сразу (например, из сети или файла):

// Асинхронный генератор
async function* fetchPages(urls) {
  for (let url of urls) {
    // Ждём загрузки каждой страницы
    const response = await fetch(url);
    const data = await response.text();
    yield data; // отдаём содержимое страницы
  }
}

// Использование с for await...of
async function processAll() {
  const urls = ['page1.html', 'page2.html', 'page3.html'];
  
  for await (let pageContent of fetchPages(urls)) {
    console.log('Получена страница длиной:', pageContent.length);
  }
}

// processAll(); // Запустите в среде с поддержкой fetch

Справочная таблица: методы итератора

Метод Описание Возвращает
next() Возвращает следующий элемент последовательности { value: any, done: boolean }
return(value) Завершает итератор досрочно, возвращает переданное значение { value: any, done: true }
throw(error) «Бросает» ошибку внутрь генератора Зависит от обработки ошибки в генераторе

Примеры кода — от простого к сложному

Пример 1: Простой счётчик

// Самый простой генератор — счётчик до 3
function* simpleCounter() {
  yield 'раз';
  yield 'два';
  yield 'три';
}

// Создаём итератор
const counter = simpleCounter();

// Вызываем next() вручную
console.log(counter.next().value); // 'раз'
console.log(counter.next().value); // 'два'
console.log(counter.next().value); // 'три'
console.log(counter.next().done);  // true — всё закончилось

Пример 2: Бесконечный генератор чётных чисел

// Генератор, который никогда не закончится
function* evenNumbers() {
  let num = 0;
  while (true) { // бесконечный цикл
    yield num;
    num += 2;
  }
}

// Берём первые 5 чётных чисел
const evens = evenNumbers();
for (let i = 0; i < 5; i++) {
  console.log(evens.next().value); // 0, 2, 4, 6, 8
}

// Важно: мы не храним все числа в памяти!
// Генератор выдаёт их по одному по требованию

Пример 3: Генератор чисел Фибоначчи

// Классическая задача — числа Фибоначчи
function* fibonacci() {
  let a = 0, b = 1;
  
  while (true) {
    yield a;          // отдаём текущее число
    [a, b] = [b, a + b]; // следующая пара
  }
}

// Получаем первые 10 чисел Фибоначчи
const fib = fibonacci();
const firstTen = [];
for (let i = 0; i < 10; i++) {
  firstTen.push(fib.next().value);
}
console.log(firstTen); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

// Объяснение строки [a, b] = [b, a + b]:
// Это деструктуризация. Создаётся временный массив [b, a+b],
// потом его значения присваиваются a и b.
// Старое значение a становится b, а b становится суммой.

Пример 4: Генератор для обхода дерева (в глубину)

// Представьте, что у нас есть дерево папок
const fileSystem = {
  name: 'root',
  children: [
    {
      name: 'docs',
      children: [
        { name: 'readme.md', children: [] },
        { name: 'manual.pdf', children: [] }
      ]
    },
    {
      name: 'src',
      children: [
        { name: 'index.js', children: [] },
        { 
          name: 'utils', 
          children: [
            { name: 'helpers.js', children: [] }
          ]
        }
      ]
    }
  ]
};

// Генератор для обхода в глубину (DFS)
function* walkTree(node) {
  yield node.name; // сначала отдаём имя текущего узла
  
  // Потом обходим всех детей
  for (let child of node.children) {
    yield* walkTree(child); // рекурсивно вызываем себя
  }
}

// Используем
for (let name of walkTree(fileSystem)) {
  console.log(name);
}
// Вывод: root, docs, readme.md, manual.pdf, src, index.js, utils, helpers.js

Пример 5: Генератор для пагинации (постраничный вывод)

// Представьте, что у нас есть база данных с 1000 пользователей,
// но мы можем получать только по 10 за раз

function* paginateUsers(pageSize = 10) {
  let page = 1;
  let hasMore = true;
  
  while (hasMore) {
    // Имитация запроса к базе данных
    console.log(`Загружаем страницу ${page}...`);
    
    // Создаём фейковые данные для примера
    const users = [];
    const startId = (page - 1) * pageSize + 1;
    
    for (let i = 0; i < pageSize; i++) {
      const userId = startId + i;
      if (userId > 100) { // всего 100 пользователей
        hasMore = false;
        break;
      }
      users.push({ id: userId, name: `User ${userId}` });
    }
    
    yield users; // отдаём страницу пользователей
    page++;
  }
}

// Используем
const userPaginator = paginateUsers(10);
for (let page of userPaginator) {
  console.log(`Получено пользователей: ${page.length}`);
  // Здесь можно отобразить страницу на сайте
  // break; // можно остановиться в любой момент
}

Пример 6: Генератор с фильтрацией и трансформацией

// Генератор, который принимает другой итератор и преобразует данные
function* filterAndTransform(source, predicate, transform) {
  for (let item of source) {
    if (predicate(item)) { // проверяем условие
      yield transform(item); // преобразуем и отдаём
    }
  }
}

// Исходные данные
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

// Создаём цепочку: фильтруем чётные и умножаем на 10
const processed = filterAndTransform(
  numbers,
  (n) => n % 2 === 0, // только чётные
  (n) => n * 10        // умножаем на 10
);

console.log([...processed]); // [20, 40, 60, 80, 100]

// Преимущество: мы не создаём промежуточных массивов!
// Всё делается "на лету"

Пример 7: Асинхронный генератор для чтения файла построчно

// В Node.js можно читать файл построчно с помощью генератора
const fs = require('fs');
const readline = require('readline');

async function* readFileLines(filePath) {
  // Создаём поток чтения
  const fileStream = fs.createReadStream(filePath);
  
  // Создаём интерфейс для чтения по строкам
  const rl = readline.createInterface({
    input: fileStream,
    crlfDelay: Infinity
  });
  
  // Перебираем строки асинхронно
  for await (const line of rl) {
    yield line; // отдаём каждую строку
  }
}

// Использование (замените 'example.txt' на реальный файл)
async function processFile() {
  try {
    const lineGenerator = readFileLines('example.txt');
    
    let lineNumber = 0;
    for await (const line of lineGenerator) {
      lineNumber++;
      console.log(`Строка ${lineNumber}: ${line}`);
      
      // Можно остановиться в любой момент
      if (lineNumber >= 5) {
        console.log('Прочитали первые 5 строк, хватит');
        break;
      }
    }
  } catch (error) {
    console.error('Ошибка при чтении файла:', error.message);
  }
}

// processFile(); // Раскомментируйте для запуска

Сравнение с Python: чем похоже и чем отличается

Python — ещё один язык, где итераторы и генераторы являются ключевой концепцией. Если вы знакомы с Python, вам будет легче понять JavaScript, и наоборот.

Характеристика JavaScript (Node.js) Python
Объявление генератора function* gen() { yield value; } def gen(): yield value
Ключевое слово yield yield
Делегирование yield* otherGenerator() yield from other_generator()
Создание итератора Вызов функции-генератора: gen() Вызов функции-генератора: gen()
Получение следующего значения iterator.next().value next(iterator)
Цикл перебора for...of for...in (но чаще просто for item in iterable)
Асинхронные генераторы async function* + for await...of async def + async for
Метод send() Нет (можно эмулировать через next(value) в некоторых реализациях) Есть: generator.send(value)
Сбор всех значений [...gen()] или Array.from(gen()) list(gen())
Типичное использование Обработка потоков данных, работа с DOM, асинхронные операции Обработка больших данных, ленивые вычисления, пайплайны

Ключевые отличия

1. Синтаксис объявления: В JavaScript нужна звёздочка * после function. В Python — просто ключевое слово def, а наличие yield делает функцию генератором автоматически.

2. Метод send(): В Python генераторы — это «двусторонние» каналы. Вы можете не только получать из них значения, но и отправлять данные обратно через send(). В JavaScript такой возможности нет (хотя можно эмулировать через замыкания).

3. Асинхронность: В JavaScript асинхронные генераторы появились раньше и используются чаще, особенно в Node.js для работы с потоками. В Python они тоже есть, но применяются реже.

4. Протокол итерации: В JavaScript протокол строже — нужен явный метод Symbol.iterator. В Python достаточно определить методы __iter__ и __next__.

💡 Совет: Если вы знаете Python, представляйте yield* в JS как yield from в Python. А for...of — как обычный for item in iterable.

Типичные ошибки и подводные камни

Ошибка 1: Итератор можно использовать только один раз

function* myGen() {
  yield 1;
  yield 2;
}

const gen = myGen();

// Первый проход — всё ок
console.log([...gen]); // [1, 2]

// Второй проход — пусто!
console.log([...gen]); // [] — генератор уже исчерпан!

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

Ошибка 2: Забыли вызвать генератор (вернули функцию, а не итератор)

function* myGen() {
  yield 1;
}

// Ошибка: забыли скобки
for (let val of myGen) { // ❌ myGen — это функция, а не итератор
  console.log(val);
}
// TypeError: myGen is not iterable

// Правильно:
for (let val of myGen()) { // ✅ вызываем функцию
  console.log(val);
}

Ошибка 3: Бесконечный цикл без условия выхода

function* infinite() {
  let i = 0;
  while (true) {
    yield i++;
  }
}

// Опасный код:
for (let num of infinite()) { // будет работать вечно!
  console.log(num);
  // Нет break!
}

Решение: Всегда предусматривайте условие выхода или используйте break.

Ошибка 4: Путаница между yield и return

function* wrong() {
  yield 1;
  return 2; // return завершает генератор
  yield 3; // этот код никогда не выполнится!
}

const gen = wrong();
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: true } — return вернул значение и завершил
console.log(gen.next()); // { value: undefined, done: true }

Ошибка 5: Попытка использовать генератор как массив

function* myGen() {
  yield 1;
  yield 2;
}

const gen = myGen();

// Неправильно:
console.log(gen[0]); // undefined — у итератора нет индексов
console.log(gen.length); // undefined

// Правильно: преобразовать в массив
const arr = [...gen];
console.log(arr[0]); // 1
console.log(arr.length); // 2

Ошибка 6: Игнорирование done: true

function* threeValues() {
  yield 'a';
  yield 'b';
  yield 'c';
}

const gen = threeValues();

// Неправильный ручной перебор:
let result = gen.next();
while (result.value) { // ❌ если value будет falsy (0, null, false), цикл прервётся
  console.log(result.value);
  result = gen.next();
}

// Правильно:
while (!result.done) {
  console.log(result.value);
  result = gen.next();
}

Ошибка 7: Забыли про асинхронность

// Неправильно: пытаемся использовать await в обычном генераторе
function* wrongAsync() {
  const data = await fetch('url'); // ❌ SyntaxError
  yield data;
}

// Правильно: async function*
async function* correctAsync() {
  const data = await fetch('url'); // ✅
  yield data;
}

Лучшие практики и советы профессионалов

1. Используйте генераторы для ленивых вычислений

Если вы работаете с большими данными (миллионы записей), никогда не загружайте их в память целиком. Используйте генераторы для обработки «на лету».

// Плохо: загружаем всё в память
function processBad(data) {
  const filtered = data.filter(x => x > 10);
  const mapped = filtered.map(x => x * 2);
  return mapped;
}

// Хорошо: обрабатываем потоком
function* processGood(data) {
  for (let item of data) {
    if (item > 10) {
      yield item * 2;
    }
  }
}

2. Комбинируйте генераторы для создания пайплайнов

Генераторы отлично компонуются. Вы можете создать цепочку преобразований, где каждый шаг — отдельный генератор.

// Пайплайн обработки данных
function* readData() { /* читаем из источника */ }
function* filterData(source) { /* фильтруем */ }
function* transformData(source) { /* преобразуем */ }
function* aggregateData(source) { /* агрегируем */ }

// Собираем цепочку
const pipeline = aggregateData(
  transformData(
    filterData(
      readData()
    )
  )
);

for (let result of pipeline) {
  console.log(result);
}

3. Используйте генераторы для создания уникальных ID

// Генератор уникальных идентификаторов
function* createIdGenerator(prefix = 'id') {
  let counter = 0;
  while (true) {
    yield `${prefix}_${counter++}_${Date.now()}`;
  }
}

const ids = createIdGenerator('user');
console.log(ids.next().value); // user_0_1700000000000
console.log(ids.next().value); // user_1_1700000000001

4. Всегда обрабатывайте ошибки в генераторах

function* safeGenerator() {
  try {
    while (true) {
      const value = yield;
      // какая-то работа
    }
  } catch (error) {
    console