Итераторы и генераторы в 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