Итерируемые объекты являются обобщением массивов. Это концепция, которая позволяет нам сделать любой объект пригодным для использования в цикле for..of
.
Конечно, массивы являются итерируемыми. Но существует множество других встроенных объектов, которые также можно итерировать. Например, строки также являются итерируемыми.
Если объект технически не является массивом, а представляет собой коллекцию (список, набор) чего-либо, то for..of
— отличный синтаксис для его обхода, поэтому давайте посмотрим, как заставить его работать.
Мы можем легко понять концепцию итераций, создав свою собственную.
Например, у нас есть объект, который не является массивом, но выглядит подходящим для for..of
.
Подобно объекту range
, который представляет интервал чисел:
пусть диапазон = { из: 1, до: 5 }; // Мы хотим, чтобы for..of работал: // for(пусть число диапазона) ... num=1,2,3,4,5
Чтобы сделать объект range
итерируемым (и, таким образом, позволить for..of
работать), нам нужно добавить к объекту метод с именем Symbol.iterator
(специальный встроенный символ специально для этого).
Когда for..of
запускается, он вызывает этот метод один раз (или выдает ошибку, если не найден). Метод должен возвращать итератор — объект с методом next
.
Далее, for..of
работает только с этим возвращенным объектом .
Когда for..of
хочет получить следующее значение, он вызывает next()
для этого объекта.
Результат next()
должен иметь форму {done: Boolean, value: any}
, где done=true
означает, что цикл завершен, в противном случае value
— это следующее значение.
Вот полная реализация range
с примечаниями:
пусть диапазон = { из: 1, до: 5 }; // 1. вызов for..of изначально вызывает это диапазон[Символ.итератор] = функция() { // ...он возвращает объект итератора: // 2. Далее, for..of работает только с объектом итератора ниже, запрашивая у него следующие значения возвращаться { текущий: this.from, последнее: this.to, // 3. next() вызывается на каждой итерации цикла for..of следующий() { // 4. он должен вернуть значение как объект {done:.., value :...} если (this.current <= this.last) { return { сделано: ложь, значение: this.current++ }; } еще { возврат {готово: правда}; } } }; }; // теперь это работает! for (пусть число диапазона) { оповещение (число); // 1, затем 2, 3, 4, 5 }
Обратите внимание на основную особенность итераций: разделение задач.
Сам range
не имеет метода next()
.
Вместо этого посредством вызова range[Symbol.iterator]()
создается другой объект, так называемый «итератор», а его next()
генерирует значения для итерации.
Таким образом, объект итератора отделен от объекта, который он выполняет итерацию.
Технически мы можем объединить их и использовать сам range
в качестве итератора, чтобы упростить код.
Так:
пусть диапазон = { из: 1, до: 5, [Символ.итератор]() { это.текущий = это.от; верните это; }, следующий() { если (this.current <= this.to) { return { сделано: ложь, значение: this.current++ }; } еще { возврат {готово: правда}; } } }; for (пусть число диапазона) { оповещение (число); // 1, затем 2, 3, 4, 5 }
Теперь range[Symbol.iterator]()
возвращает сам объект range
: он имеет необходимый метод next()
и запоминает текущий прогресс итерации в this.current
. Короче? Да. И иногда это тоже хорошо.
Обратной стороной является то, что теперь невозможно одновременно выполнять два цикла for..of
над объектом: они будут использовать общее состояние итерации, поскольку существует только один итератор — сам объект. Но два параллельных for-of — редкость, даже в асинхронных сценариях.
Бесконечные итераторы
Также возможны бесконечные итераторы. Например, range
становится бесконечным для range.to = Infinity
. Или мы можем создать итерируемый объект, который генерирует бесконечную последовательность псевдослучайных чисел. Также может быть полезно.
На next
нет ограничений, он может возвращать все больше и больше значений, это нормально.
Конечно, цикл for..of
для такой итерации будет бесконечным. Но мы всегда можем остановить это, используя break
.
Массивы и строки являются наиболее широко используемыми встроенными итерируемыми объектами.
Для строки for..of
циклически перебирает ее символы:
for (let char of "test") { // срабатывает 4 раза: по одному разу для каждого символа предупреждение (символ); // t, затем e, затем s, затем t }
И с суррогатными парами это работает корректно!
пусть str = '??'; for (пусть char of str) { предупреждение (символ); // ?, а потом ? }
Для более глубокого понимания давайте посмотрим, как явно использовать итератор.
Мы будем перебирать строку точно так же, как и for..of
, но с прямыми вызовами. Этот код создает строковый итератор и получает от него значения «вручную»:
let str = "Привет"; // делает то же самое, что и // for (let char of str) alert(char); пусть итератор = str[Symbol.iterator](); в то время как (истина) { пусть результат = iterator.next(); если (result.done) сломать; оповещение(результат.значение); // выводит символы один за другим }
Это требуется редко, но дает нам больше контроля над процессом, чем for..of
. Например, мы можем разделить итерационный процесс: выполнить небольшую итерацию, затем остановиться, сделать что-то еще и возобновить позже.
Два официальных термина выглядят одинаково, но сильно отличаются. Пожалуйста, убедитесь, что вы их хорошо понимаете, чтобы избежать путаницы.
Итерируемые объекты — это объекты, реализующие метод Symbol.iterator
, как описано выше.
Подобные массивам — это объекты, которые имеют индексы и length
, поэтому они выглядят как массивы.
Когда мы используем JavaScript для практических задач в браузере или любой другой среде, мы можем встретить объекты, которые являются итерируемыми или подобными массиву, или и тем, и другим.
Например, строки являются как итерируемыми ( for..of
работ над ними), так и массивными (у них есть числовые индексы и length
).
Но итерация может не быть массивом. И наоборот, массив может быть неперебираемым.
Например, range
в приведенном выше примере является итеративным, но не похож на массив, поскольку у него нет индексированных свойств и length
.
А вот объект, похожий на массив, но не итерируемый:
let arrayLike = { // имеет индексы и длину => как массив 0: «Привет», 1: «Мир», длина: 2 }; // Ошибка (нет символа.итератора) for (пусть элемент arrayLike) {}
И итерации, и подобные массивам обычно не являются массивами , у них нет push
, pop
и т. д. Это довольно неудобно, если у нас есть такой объект и мы хотим работать с ним как с массивом. Например, мы хотели бы работать с range
, используя методы массива. Как этого добиться?
Существует универсальный метод Array.from, который принимает итерируемое или подобное массиву значение и создает из него «настоящий» Array
. Затем мы можем вызвать на нем методы массива.
Например:
пусть arrayLike = { 0: «Привет», 1: «Мир», длина: 2 }; пусть arr = Array.from(arrayLike); // (*) оповещение(arr.pop()); // Мир (метод работает)
Array.from
в строке (*)
берет объект, проверяет его на предмет итерируемости или сходства с массивом, затем создает новый массив и копирует в него все элементы.
То же самое происходит и с итерацией:
// предполагая, что диапазон взят из примера выше пусть arr = Array.from(диапазон); оповещение (прибытие); // 1,2,3,4,5 (преобразование массива в строку работает)
Полный синтаксис Array.from
также позволяет нам предоставить дополнительную функцию «сопоставления»:
Array.from(obj[,mapFn, thisArg])
Необязательный второй аргумент mapFn
может быть функцией, которая будет применяться к каждому элементу перед добавлением его в массив, и thisArg
позволяет нам установить this
для него.
Например:
// предполагая, что диапазон взят из примера выше // возводим в квадрат каждое число пусть arr = Array.from(range, num => num * num); оповещение (прибытие); // 1,4,9,16,25
Здесь мы используем Array.from
, чтобы превратить строку в массив символов:
пусть str = '??'; // разбивает строку на массив символов пусть символы = Array.from(str); предупреждение (символы [0]); // ? предупреждение (символы [1]); // ? оповещение(chars.length); // 2
В отличие от str.split
, он опирается на итеративный характер строки и поэтому, как и for..of
, корректно работает с суррогатными парами.
Технически здесь он делает то же самое, что и:
пусть str = '??'; пусть символы = []; // Array.from внутренне выполняет тот же цикл for (пусть char of str) { символы.push(символ); } предупреждение (символы);
…Но он короче.
Мы даже можем создать на его основе суррогатный slice
:
функция срез (строка, начало, конец) { return Array.from(str).slice(start, end).join(''); } пусть str = '???'; Предупреждение( срез (строка, 1, 3) ); // ?? // собственный метод не поддерживает суррогатные пары предупреждение(str.slice(1, 3)); // мусор (две штуки из разных суррогатных пар)
Объекты, которые можно использовать в for..of
называются итерируемыми .
Технически итерации должны реализовывать метод с именем Symbol.iterator
.
Результат obj[Symbol.iterator]()
называется итератором . Он обрабатывает дальнейший процесс итерации.
Итератор должен иметь метод с именем next()
, который возвращает объект {done: Boolean, value: any}
, здесь done:true
обозначает конец итерационного процесса, в противном случае value
является следующее значение.
Метод Symbol.iterator
вызывается автоматически for..of
, но мы также можем сделать это напрямую.
Встроенные итерации, такие как строки или массивы, также реализуют Symbol.iterator
.
Строковый итератор знает о суррогатных парах.
Объекты, которые имеют индексированные свойства и length
, называются массивоподобными . Такие объекты также могут иметь другие свойства и методы, но не иметь встроенных методов массивов.
Если мы заглянем внутрь спецификации, то увидим, что большинство встроенных методов предполагают, что они работают с итерациями или подобными массивам вместо «настоящих» массивов, потому что это более абстрактно.
Array.from(obj[, mapFn, thisArg])
создает настоящий Array
из итерируемого или подобного массиву объекта obj
, и затем мы можем использовать к нему методы массива. Необязательные аргументы mapFn
и thisArg
позволяют нам применить функцию к каждому элементу.