Итерируемые объекты и итераторы в JavaScript
Итерируемые объекты — ключевая концепция JavaScript. Изучите протокол итератора, встроенные итерируемые типы и практические приёмы работы с коллекциями.
Введение в итерируемые объекты JavaScript
Итерируемые объекты JavaScript — это объекты, реализующие специальный протокол, позволяющий использовать их в конструкциях итерации, таких как for...of. В этом руководстве рассматриваются ключевые концепции, встроенные итерируемые типы и практические приёмы работы с коллекциями.
Что такое итерируемые объекты в JavaScript?
По своей сути итерируемый объект — это объект, реализующий метод Symbol.iterator, обеспечивающий последовательный доступ к его элементам. Ряд встроенных типов в JavaScript является итерируемым, включая Array, String, Map, Set и другие. Важно различать итерируемый объект (объект, по которому выполняется обход) и итератор (объект, возвращаемый методом Symbol.iterator, который фактически выполняет обход). Итерируемые объекты являются неотъемлемой частью различных операций — цикличного обхода и манипуляций с данными.
Подробное описание объектов JavaScript Map и Set приведено в нашем полном руководстве JavaScript Map and Set.
Пример итерируемого объекта: Array
Рассмотрим простой пример итерируемого объекта в JavaScript:
Этот фрагмент кода демонстрирует итерацию по array фруктов — типичному итерируемому объекту. Цикл for...of автоматически вызывает метод Symbol.iterator итерируемого объекта и потребляет полученный итератор до тех пор, пока done не станет равным true. Полный обзор семейства циклов см. в разделе Циклы JavaScript.
Не путайте for...of и for...in. for...of перебирает значения, производимые итерируемым объектом (элементы array, символы string, записи Map). for...in перебирает перечисляемые ключи свойств объекта — включая унаследованные — и предназначен для обычных объектов, а не array. Применение for...in к array даёт строковые индексы ("0", "1", …) и может захватить лишние свойства, поэтому для упорядоченных данных предпочтительнее использовать for...of.
Протокол итератора
Основой итерируемого объекта служит его метод Symbol.iterator. Протокол — это точный контракт:
- Итерируемый объект имеет метод с ключом
Symbol.iterator. При вызове он возвращает итератор. - Итератор — это объект с методом
next(). - Каждый вызов
next()возвращает объект{ value, done }:done: false—valueявляется следующим элементом последовательности.done: true— итерация завершена (valueв этом случае игнорируется или содержит необязательный финальный результат).
Любой объект, следующий этому контракту, работает с for...of, оператором spread, деструктурированием и Array.from() — даже если вы написали объект самостоятельно. Symbol — это встроенный уникальный ключ; о том, почему методы протокола используют его вместо обычного строкового имени, см. в разделе Тип Symbol.
Пример: пользовательский итерируемый объект range
Классический вариант использования — объект, лениво генерирующий числовой диапазон без построения array:
В этом примере метод [Symbol.iterator]() каждый раз возвращает свежий итератор, поэтому один и тот же объект range можно обходить более одного раза. Сокращённый синтаксис метода гарантирует, что this корректно ссылается на объект range. (Использование стрелочной функции для [Symbol.iterator] привязало бы this лексически и нарушило бы паттерн.)
Генераторы: простой способ создания итерируемых объектов
Написание next() и ручное отслеживание состояния — громоздко. Функция-генератор — объявленная с помощью function* и использующая yield — автоматически создаёт итератор. Каждый yield приостанавливает функцию и передаёт значение потребителю; выполнение возобновляется при следующем вызове next(). Тот же объект range становится значительно короче:
Символ * перед именем метода делает его методом-генератором, поэтому range становится итерируемым практически без шаблонного кода. Полное описание возможностей генераторов — включая двустороннюю коммуникацию и делегирование с помощью yield* — см. в разделах Генераторы и Асинхронные итераторы и генераторы.
Бесконечные и ленивые последовательности
Поскольку итератор вычисляет следующее значение только по запросу, он может описывать последовательности, которые слишком велики — или даже бесконечны — чтобы хранить их в памяти. Именно по этой причине стоит писать пользовательский итератор вместо обычного array:
Никогда не используйте spread ([...naturals()]) или for...of без break с бесконечным итерируемым объектом — это приведёт к бесконечному циклу. Вместо этого получайте конечное число значений с помощью next().
Потребление итерируемых объектов
Как только объект становится итерируемым, весь язык открывается для него: любая конструкция, принимающая итерируемый объект, работает с вашим пользовательским типом так же, как с array.
Использование Array.from()
Метод Array.from() создаёт новый array из любого итерируемого (или массивоподобного) объекта. Он также принимает необязательную функцию отображения в качестве второго аргумента, применяемую к каждому элементу при построении array — это удобнее, чем Array.from(it).map(fn), поскольку позволяет избежать второго прохода:
Подробнее о Set см. в разделе JavaScript Map and Set, а о доступных операциях с array — в разделе Методы array.
Синтаксис spread с итерируемыми объектами
Синтаксис spread (...) разворачивает итерируемый объект там, где ожидаются аргументы или элементы — для объединения array, копирования или передачи элементов в качестве аргументов функции:
Полное описание применения ... как в позиции spread, так и в позиции сбора значений см. в разделе Остаточные параметры и синтаксис spread.
Деструктурирование и остаточные элементы
Деструктурирующее присваивание извлекает значения из любого итерируемого объекта по позиции, а паттерн rest (...) собирает оставшиеся значения в array:
Подробнее см. в разделе Деструктурирующее присваивание.
Итерируемые строки и безопасная работа с Unicode
Строки итерируемы, и, что важно, итератор обходит кодовые точки Unicode, а не 16-битные единицы кода. Это означает, что символы суррогатных пар (эмодзи, некоторые письменности) сохраняются целиком — в отличие от индексирования через [i] или старых циклов for по .length, которые могут их разбивать:
Когда нужно корректно подсчитать или разбить символы, видимые пользователю, итерируйте строку (или разворачивайте её через spread) вместо того, чтобы опираться на .length. Подробнее см. в главе Строки.
Итоги
- Объект является итерируемым, если он имеет метод
[Symbol.iterator](), возвращающий итератор — объект, чейnext()выдаёт{ value, done }. - Используйте генератор (
function*/yield) вместо написанияnext()вручную — это самый краткий и надёжный способ сделать что-либо итерируемым. - Используйте
for...ofдля значений упорядоченных/итерируемых данных;for...in— только для ключей обычных объектов. - Став итерируемым, ваш тип бесплатно получает поддержку spread, деструктурирования, rest,
Array.from(it, mapFn)и многих встроенных API. - Итераторы ленивы, поэтому они могут моделировать бесконечные или очень большие последовательности, которые array никогда бы не смог вместить.
- Итерируйте строки (не индексируйте их), чтобы безопасно обрабатывать символы Unicode, такие как эмодзи.