Итераторы в JavaScript: подробное руководство с примерами
Symbol.iterator, next(), for...of? Создание своих итераторов и ленивые вычисления. С примерами кода и ссылками на смежные статьи.Введение
Если вы писали на JavaScript хотя бы несколько недель, вы уже пользовались итераторами. Каждый раз, когда вы используете цикл for...of для массива или разворачиваете строку в массив через спред-оператор ([...'строка']), вы полагаетесь на механизм итерации. Он работает надёжно, незаметно и, чаще всего, именно так, как вы ожидаете.
Проблема возникает в момент, когда код ведёт себя неожиданно. Или когда вы читаете документацию и встречаете термины «итерируемый объект» (iterable), «итератор» (iterator), «протокол итерации» и загадочный Symbol.iterator. В этот момент тема перестаёт быть магией «под капотом» и превращается в препятствие.
Эта статья — не попытка развлечь читателя сложной темой. Это структурированное объяснение того, как именно работает итерация в JavaScript. Мы разберём:
- Чем итерируемый объект отличается от итератора и почему это важно.
- Как устроен метод
next()и что на самом деле означают поляvalueиdone. - Где проходит граница между однократным перебором (итератор) и многократным (итерируемый объект).
- И, главное — как использовать эти знания на практике, чтобы писать более предсказуемый и эффективный код.
Для дальнейшего изучения:
- Если вы хотите понять, как итераторы связаны с циклами и какой цикл когда выбирать — в материале «Всё о циклах в JavaScript» есть полный разбор всех вариантов.
- Когда освоите базу, обратите внимание на «Set и Map» — это основные итерируемые коллекции, с которыми вы будете работать чаще всего.
Никакой воды, только термины, примеры и их практический смысл.
Основные понятия: итерируемые объекты и итераторы
Прежде чем углубляться в детали, необходимо чётко разделить два понятия, которые в разговорной речи часто смешивают, но в спецификации JavaScript они строго разграничены: итерируемый объект (iterable) и итератор (iterator).
Итерируемый объект — это структура данных, которая заявляет: «меня можно перебирать». Чтобы это заявление имело силу, объект должен следовать протоколу итерируемого объекта. Требование протокола одно: наличие метода с ключом Symbol.iterator в цепочке прототипов. Этот метод при вызове обязан вернуть итератор.
К итерируемым объектам в JavaScript относятся:
- Массивы (
Array) - Строки (
String) SetMapNodeList(в браузере)- Объекты, созданные вами с явной реализацией
[Symbol.iterator]()
Итератор — это объект, который непосредственно выполняет перебор. Он следует протоколу итератора. Требование протокола: наличие метода next(). Этот метод при каждом вызове возвращает объект с двумя полями:
value— текущий элемент последовательности (илиundefined, если перебор завершён).done— логическое значение, указывающее, завершён ли перебор.
Чтобы закрепить разницу, рассмотрим простую аналогию.
Представьте, что книга — это итерируемый объект. Книгу можно читать сколько угодно раз, с начала до конца. Закладка, которую вы перемещаете по страницам, — это итератор. Она движется только вперёд, запоминает текущую позицию и, дойдя до последней страницы, фиксирует, что книга закончена. Если вы хотите перечитать книгу, вам нужна новая закладка — новый итератор.
Или, в терминах кода:
// Итерируемый объект (книга)
const book = ['Страница 1', 'Страница 2', 'Страница 3'];
// Создаём итератор (закладку)
const bookmark = book[Symbol.iterator]();
// Читаем страницы последовательно
bookmark.next(); // { value: 'Страница 1', done: false }
bookmark.next(); // { value: 'Страница 2', done: false }
bookmark.next(); // { value: 'Страница 3', done: false }
bookmark.next(); // { value: undefined, done: true }Книга (book) как была, так и осталась доступной для перебора. Закладка (bookmark) исчерпала свой ресурс и больше не даст страниц, если только мы не создадим новую.
Встроенные итераторы: keys(), values() и entries()
Теперь, когда мы разобрали теорию, посмотрим, где итераторы встречаются в повседневном коде. Самый наглядный пример — методы keys(), values() и entries() у коллекций Map и Set, а также у массивов.
Возьмём Map:
const userRoles = new Map([
['admin', 'Редактор'],
['user', 'Читатель'],
['guest', 'Гость']
]);
const roles = userRoles.values();
console.log(roles);
// Результат: Map Iterator { 'Редактор', 'Читатель', 'Гость' }Метод values() возвращает не массив, а именно итератор. Это можно проверить:
console.log(roles.next());
// { value: 'Редактор', done: false }
console.log(roles.next());
// { value: 'Читатель', done: false }
console.log(roles.next());
// { value: 'Гость', done: false }
console.log(roles.next());
// { value: undefined, done: true }Почему это удобно? Итератор не создаёт новый массив со всеми значениями сразу. Он выдаёт значения по одному, по мере необходимости. Если в Map миллион записей, values() не копирует их все в память — это экономит ресурсы.
Важный нюанс, который часто сбивает с толку: сам итератор (roles) тоже является итерируемым объектом. То есть у него есть метод [Symbol.iterator](), который возвращает его же. Это сделано для совместимости: вы можете использовать итератор в цикле for...of так же, как и исходный Map.
const roles = userRoles.values();
for (const role of roles) {
console.log(role); // 'Редактор', 'Читатель', 'Гость'
}
// Но повторный проход не сработает — итератор уже исчерпан
for (const role of roles) {
console.log(role); // Ничего не выведет
}Здесь проявляется ключевое различие, о котором мы говорили ранее: итератор хранит состояние. Первый цикл прошёл по всем значениям и «сдвинул» внутренний счётчик итератора до конца. Второй цикл получает уже исчерпанный итератор.
Создание итераторов вручную: Symbol.iterator и Iterator.from
До сих пор мы имели дело со встроенными итераторами, которые JavaScript создаёт за нас. Но чтобы действительно понять механизм, полезно научиться создавать их самостоятельно. Это также открывает возможность делать любые свои объекты итерируемыми.
Явный вызов Symbol.iterator
Любой итерируемый объект обязан иметь метод с ключом Symbol.iterator. Вызов этого метода возвращает новый итератор. Это именно то, что JavaScript делает неявно, когда вы пишете for...of.
const fruits = ['яблоко', 'банан', 'апельсин'];
// Получаем итератор напрямую
const fruitIterator = fruits[Symbol.iterator]();
console.log(fruitIterator.next()); // { value: 'яблоко', done: false }
console.log(fruitIterator.next()); // { value: 'банан', done: false }
console.log(fruitIterator.next()); // { value: 'апельсин', done: false }
console.log(fruitIterator.next()); // { value: undefined, done: true }Это работает не только с массивами, но и с любым итерируемым объектом: строкой, Set, Map.
const title = 'Код';
const charIterator = title[Symbol.iterator]();
console.log(charIterator.next()); // { value: 'К', done: false }
console.log(charIterator.next()); // { value: 'о', done: false }
console.log(charIterator.next()); // { value: 'д', done: false }
console.log(charIterator.next()); // { value: undefined, done: true }Современный подход: Iterator.from()
Начиная с ES2024 (реализован во всех современных браузерах), появился более удобный способ создать итератор — статический метод Iterator.from(). Он принимает любой итерируемый объект и возвращает итератор.
const fruits = ['яблоко', 'банан', 'апельсин'];
const fruitIterator = Iterator.from(fruits);
fruitIterator.next(); // { value: 'яблоко', done: false }Преимущество этого метода в читаемости: он явно говорит о том, что мы создаём итератор, и не требует прямого обращения к Symbol.iterator.
Создание собственных итерируемых объектов
Понимание этого механизма позволяет сделать итерируемым любой объект. Достаточно реализовать метод [Symbol.iterator], который возвращает объект с методом next().
const range = {
start: 1,
end: 5,
[Symbol.iterator]() {
let current = this.start;
const last = this.end;
return {
next() {
if (current <= last) {
return { value: current++, done: false };
}
return { value: undefined, done: true };
}
};
}
};
// Теперь range можно использовать в for...of
for (const num of range) {
console.log(num); // 1, 2, 3, 4, 5
}Этот пример демонстрирует главное: итератор — это просто объект с методом next(), который каждый раз возвращает следующее значение и сигнализирует о завершении перебора.
Практическое применение: методы итераторов
Долгое время работа с итераторами вручную была довольно многословной: получить итератор, вызвать next() в нужный момент, проверить done. Современный JavaScript предоставляет встроенные методы, которые делают работу с итераторами такой же удобной, как с массивами, но без лишних затрат памяти.
Базовые методы: forEach, map, filter
У итераторов есть собственная версия привычных методов массивов:
const users = Iterator.from([
{ name: 'Анна', role: 'admin' },
{ name: 'Борис', role: 'user' },
{ name: 'Виктор', role: 'user' }
]);
// forEach — выполнить действие для каждого элемента
users.forEach(user => {
console.log(`${user.name} (${user.role})`);
});
// Важно: после forEach итератор исчерпан
users.next(); // { value: undefined, done: true }Методы map(), filter() и reduce() работают аналогично:
const numbers = Iterator.from([1, 2, 3, 4, 5]);
const doubled = numbers.map(n => n * 2);
console.log([...doubled]); // [2, 4, 6, 8, 10]
// Но исходный итератор numbers уже пустМетоды для работы с последовательностью
Наиболее интересны методы, которые используют ключевую особенность итераторов — их поточность:
const logs = Iterator.from([
'2025-01-10: Ошибка A',
'2025-01-10: Успех B',
'2025-01-11: Ошибка C',
'2025-01-11: Ошибка D',
'2025-01-12: Успех E'
]);
// take(N) — взять только первые N элементов
const firstThree = logs.take(3);
console.log([...firstThree]);
// ['2025-01-10: Ошибка A', '2025-01-10: Успех B', '2025-01-11: Ошибка C']
// logs теперь указывает на оставшиеся элементы: ['2025-01-11: Ошибка D', '2025-01-12: Успех E']
// drop(N) — пропустить N элементов и взять остальные
const remainingAfterDrop = logs.drop(1);
console.log([...remainingAfterDrop]);
// ['2025-01-12: Успех E']Методы take, drop, filter — это лишь вершина айсберга. Полный обзор всех ленивых методов итераторов, их сравнение с методами массивов и глубокий разбор производительности — в статье «Хелперы итераторов в JavaScript: ленивые вычисления вместо лишних массивов». Там же вы найдёте примеры работы с асинхронными итераторами и пагинацией API.
Реальный сценарий: постраничная загрузка
Представьте, что вы получаете с сервера ленту событий, но отображаете их постранично, по 10 элементов. Вместо того чтобы хранить все записи в массиве и вручную вычислять срезы, можно использовать итератор:
class EventFeed {
constructor(events) {
this.events = events;
}
getPage(pageSize) {
// Создаём новый итератор от всего списка
const iterator = Iterator.from(this.events);
// Берём первые pageSize элементов
return iterator.take(pageSize);
}
}
const feed = new EventFeed(большойМассивСобытий);
// Первая страница
const page1 = feed.getPage(10);
// Вторая страница — итератор создаётся заново, всё корректно
const page2 = feed.getPage(10);Преимущество: итератор не копирует элементы, он просто знает, какие из них нужно вернуть. Для больших данных это может дать прирост производительности.
Концепция «ленивой» обработки, которую мы здесь применили, раскрыта в деталях в статье про хелперы итераторов. Особенно обратите внимание на раздел про асинхронные итераторы — там показано, как не загружать лишние страницы с API.
Важное ограничение
Все методы итераторов мутируют итератор. То есть после вызова take(), drop() или filter() исходный итератор продвигается вперёд. Это не баг, а особенность: итератор по определению представляет текущее состояние перебора.
const numbers = Iterator.from([1, 2, 3, 4]);
const firstTwo = numbers.take(2);
numbers.next(); // { value: 3, done: false } — итератор уже на третьем элементеЕсли вам нужно сохранить исходную последовательность нетронутой, создавайте новый итератор от исходных данных.
Заключение
Мы разобрали тему, которая часто вызывает путаницу даже у опытных разработчиков. Давайте зафиксируем главное, что нужно вынести из этого материала.
Ключевые выводы
Итерируемый объект и итератор — разные сущности
- Итерируемый объект (массив, строка,
Set,Map) — это источник данных, который можно перебирать многократно. Его задача — предоставлять итераторы. - Итератор — это механизм для однократного прохода по данным. Он хранит текущее положение в последовательности и уничтожается (исчерпывается) после завершения прохода.
- Итерируемый объект (массив, строка,
Протоколы — это просто интерфейсы
- Протокол итерируемого объекта требует метод
[Symbol.iterator](). - Протокол итератора требует метод
next(), возвращающий{ value, done }. - Никакой магии, только чёткие контракты.
- Протокол итерируемого объекта требует метод
Итераторы экономят память
- Встроенные методы
keys(),values(),entries()возвращают итераторы, а не массивы. Это значит, что даже с коллекцией из миллиона элементов вы не создаёте копию всех значений в памяти. - Методы
take(),drop(),filter()работают с элементами «на лету», без создания промежуточных массивов.
- Встроенные методы
Состояние имеет значение
- Одноразовость итераторов — не недостаток, а фундаментальное свойство. Если вам нужно пройти по данным повторно, создайте новый итератор от исходного итерируемого объекта.
Благодарность и источник
Этот материал основан на статье «JavaScript For Everyone: Iterators» Mat "Wilto" Marquis , опубликованной на Smashing Magazine. Оригинал глубоко раскрывает тему итераторов в контексте обучающего курса, а мы адаптировали его для структурированного самостоятельного изучения: добавили чёткое разделение понятий, визуальные выделения, практические примеры и связали с другими материалами по JavaScript.
Благодарим автора и редакцию Smashing Magazine за качественный контент и возможность делиться знаниями с русскоязычной аудиторией.
Что дальше
Теперь, когда вы понимаете базовые механизмы итерации, вы готовы к более глубокому погружению:
Для понимания инструментов, с которыми вы будете работать чаще всего
«JavaScript Set и Map: За пределами массивов и объектов»
Почему эти коллекции существуют, когда они выгоднее массивов и объектов, и как их использовать для максимальной производительности.
Для реальных примеров из разработки
«Управляйте spread-синтаксисом: Практическое применение Symbol.iterator»
Как создать объект, который одновременно является и удобной структурой с полями, и итерируемым для spread-оператора. Готовое решение для библиотек и утилит.
Для эффективной обработки данных
«Хелперы итераторов в JavaScript: ленивые вычисления вместо лишних массивов»
Как перестать создавать промежуточные массивы в цепочках
filter.map.sliceи научиться обрабатывать ровно столько данных, сколько нужно.Для полного понимания внутренних механизмов
«JavaScript итераторы и генераторы: Полное руководство»
Ручное создание итераторов для сложных структур, генераторы как более безопасный способ, методы
returnиthrow, передача значений в генераторы.Для выбора правильного цикла
Обзор всех способов итерации: от классического
forдоfor await...of, с рекомендациями, что и когда использовать.
Практический совет
Если вы только начинаете знакомство с итераторами, не пытайтесь запомнить все детали протоколов. Запомните главное:
В следующий раз, когда будете использовать for...of или спред-оператор, вспомните, что внутри JavaScript создаёт итератор, вызывает у него next(), пока done не станет true. Это знание превращает магию в инженерию.
Часто задаваемые вопросы
В чем разница между итерируемым объектом и итератором простыми словами?
Итерируемый объект — это источник данных, который можно перебирать (массив, строка, Set, Map). Он как библиотека: книги всегда на месте, можно приходить снова и снова.
Итератор — это механизм для конкретного перебора. Он как библиотечная закладка: движется только вперёд, запоминает текущую позицию и после того, как вы дошли до последней страницы, закладка исчерпана. Чтобы перечитать книгу, нужна новая закладка.
Почему после for...of нельзя повторно пройти по тому же итератору?
Потому что итератор хранит внутреннее состояние. После того как метод next() вернул { done: true }, итератор считается исчерпанным. Это не баг, а фундаментальное свойство: итератор представляет собой процесс перебора, а процесс нельзя пройти дважды.
Если нужно перебрать данные повторно, создайте новый итератор от исходного итерируемого объекта.
Чем for...of отличается от for...in?
for...ofперебирает значения и работает только с итерируемыми объектами (массивы, строки, Set, Map).for...inперебирает ключи (имена свойств) и работает с любыми объектами, но проходит по всем enumerable-свойствам, включая унаследованные через прототип.
Для массивов for...in использовать не рекомендуется — он будет перебирать не только индексы, но и другие добавленные свойства.
Подробный разбор всех циклов — в статье «Всё о циклах в JavaScript».
Можно ли сделать итерируемым обычный объект { key: value }?
Да, для этого нужно реализовать метод [Symbol.iterator], который возвращает объект с методом next(). В статье есть пример с объектом range, а более сложный случай (вложенные данные) разобран в «JavaScript итераторы и генераторы: Полное руководство».
Но важно понимать: если вам просто нужно перебрать ключи и значения обычного объекта, проще использовать Object.keys(), Object.values() или Object.entries() в комбинации с for...of.
Что произойдёт, если в итераторе забыть обновлять done?
Итератор либо никогда не завершится (бесконечный цикл), либо завершится слишком рано. В обоих случаях поведение будет некорректным.
Пример бесконечного итератора:
const badIterator = {
next() {
return { value: 1, done: false }; // done всегда false → бесконечность
}
};При использовании такого итератора в for...of цикл никогда не остановится.
Зачем нужен Iterator.from(), если есть [Symbol.iterator]()?
Iterator.from() — это более удобная и читаемая обёртка. Вместо того чтобы вручную вызывать obj[Symbol.iterator](), вы пишете Iterator.from(obj). Кроме того, Iterator.from() гарантирует, что результат будет соответствовать протоколу итератора, и корректно обрабатывает краевые случаи (например, если переданный объект уже является итератором).
Чем методы итераторов (map, filter, take) лучше методов массивов?
Они работают лениво (lazy). Когда вы пишете array.map(...).filter(...).slice(0,10), каждый метод создаёт новый массив целиком. Даже если в итоге вам нужно всего 10 элементов, будут обработаны все.
С итераторами:
const result = array.values()
.map(transform)
.filter(isValid)
.take(10)
.toArray();Здесь элементы обрабатываются по одному, и как только набрано 10 подходящих, работа прекращается. Никаких промежуточных массивов.
Подробно с примерами и замерами производительности — в статье «Хелперы итераторов в JavaScript: ленивые вычисления».
Что такое асинхронные итераторы и for await...of?
Асинхронные итераторы (Symbol.asyncIterator) нужны для работы с потоками данных, которые поступают не сразу, а с задержкой (например, чанки от API, строки из файла, события во времени).
Они работают так же, как обычные, но метод next() возвращает не { value, done }, а Promise, который резолвится с этим объектом.
Цикл for await...of автоматически ждёт каждый Promise и передаёт значение в тело цикла.
Подробный разбор с примерами — в упомянутой выше статье про хелперы итераторов.
Поддерживаются ли итераторы в старых браузерах?
Symbol.iteratorи базовые итераторы (для массивов, строк,Set/Map) поддерживаются начиная с IE (нет) / Chrome 38 (2014) — то есть во всех современных браузерах.Iterator.from()и методы итераторов (map,filter,take) — новые возможности, поддерживаются в актуальных версиях браузеров с 2023–2024 года.- Для старых браузеров потребуется полифилл или транспиляция.
Перед использованием новых методов проверяйте поддержку на caniuse.com или используйте проверку через typeof Iterator !== 'undefined'.
Генераторы — это то же самое, что итераторы?
Генераторы (function*) — это удобный способ создавать итераторы. Вместо того чтобы вручную писать объект с next() и управлять состоянием, вы пишете функцию с yield, и JavaScript сам сгенерирует корректный итератор.
Все генераторы возвращают итераторы, но не все итераторы созданы через генераторы.
Полное руководство по генераторам — в статье «JavaScript итераторы и генераторы: Полное руководство».