В современный JavaScript добавлена новая концепция «итерируемых» (iterable) объектов.
Итерируемые или, иными словами, «перебираемые» объекты – это те, содержимое которых можно перебрать в цикле.
Например, перебираемым объектом является массив. Но не только он. В браузере существует множество объектов, которые не являются массивами, но содержимое которых можно перебрать (к примеру, список DOM-узлов).
Для перебора таких объектов добавлен новый синтаксис цикла: for..of
.
Например:
'use strict';
let arr = [1, 2, 3]; // массив — пример итерируемого объекта
for (let value of arr) {
alert(value); // 1, затем 2, затем 3
}
Также итерируемой является строка:
'use strict';
for (let char of "Привет") {
alert(char); // Выведет по одной букве: П, р, и, в, е, т
}
Итераторы – расширяющая понятие «массив» концепция, которая пронизывает современный стандарт JavaScript сверху донизу.
Практически везде, где нужен перебор, он осуществляется через итераторы. Это включает в себя не только строки, массивы, но и вызов функции с оператором spread f(...args)
, и многое другое.
В отличие от массивов, «перебираемые» объекты могут не иметь «длины» length
. Как мы увидим далее, итераторы дают возможность сделать «перебираемыми» любые объекты.
Свой итератор
Допустим, у нас есть некий объект, который надо «умным способом» перебрать.
Например, range
– диапазон чисел от from
до to
, и мы хотим, чтобы for (let num of range)
«перебирал» этот объект. При этом под перебором мы подразумеваем перечисление чисел от from
до to
.
Объект range
без итератора:
let range = {
from: 1,
to: 5
};
// хотим сделать перебор
// for (let num of range) ...
Для возможности использовать объект в for..of
нужно создать в нём свойство с названием Symbol.iterator
(системный символ).
При вызове метода Symbol.iterator
перебираемый объект должен возвращать другой объект («итератор»), который умеет осуществлять перебор.
По стандарту у такого объекта должен быть метод next()
, который при каждом вызове возвращает очередное значение и проверяет, окончен ли перебор.
В коде это выглядит следующим образом:
'use strict';
let range = {
from: 1,
to: 5
}
// сделаем объект range итерируемым
range[Symbol.iterator] = function() {
let current = this.from;
let last = this.to;
// метод должен вернуть объект с методом next()
return {
next() {
if (current <= last) {
return {
done: false,
value: current++
};
} else {
return {
done: true
};
}
}
}
};
for (let num of range) {
alert(num); // 1, затем 2, 3, 4, 5
}
Как видно из кода выше, здесь имеет место разделение сущностей:
- Перебираемый объект
range
сам не реализует методы для своего перебора. - Для этого создаётся другой объект, который хранит текущее состояние перебора и возвращает значение. Этот объект называется итератором и возвращается при вызове метода
range[Symbol.iterator]
. - У итератора должен быть метод
next()
, который при каждом вызове возвращает объект со свойствами:value
– очередное значение,done
– равноfalse
если есть ещё значения, иtrue
– в конце.
Конструкция for..of
в начале своего выполнения автоматически вызывает Symbol.iterator()
, получает итератор и далее вызывает метод next()
до получения done: true
. Такова внутренняя механика. Внешний код при переборе через for..of
видит только значения.
Такое отделение функциональности перебора от самого объекта даёт дополнительную гибкость. Например, объект может возвращать разные итераторы в зависимости от своего настроения и времени суток. Однако, бывают ситуации когда оно не нужно.
Если функциональность по перебору (метод next
) предоставляется самим объектом, то можно вернуть this
в качестве итератора:
'use strict';
let range = {
from: 1,
to: 5,
[Symbol.iterator]() {
return this;
},
next() {
if (this.current === undefined) {
// инициализация состояния итерации
this.current = this.from;
}
if (this.current <= this.to) {
return {
done: false,
value: this.current++
};
} else {
// очистка текущей итерации
delete this.current;
return {
done: true
};
}
}
};
for (let num of range) {
alert(num); // 1, затем 2, 3, 4, 5
}
// Произойдёт вызов Math.max(1,2,3,4,5);
alert( Math.max(...range) ); // 5 (*)
При таком подходе сам объект и хранит состояние итерации (текущий перебираемый элемент).
В данном случае это работает, но для большей гибкости и понятности кода рекомендуется, всё же, выделять итератор в отдельный объект со своим состоянием и кодом.
...
и итераторыВ последней строке (*)
примера выше можно видеть, что итерируемый объект передаётся через spread для Math.max
.
При этом ...range
интерпретируется как последовательность чисел. То есть произойдёт цикл for..of
по range
, и его результаты будут использованы в качестве списка аргументов.
Возможны и бесконечные итераторы. Например, пример выше при range.to = Infinity
будет таковым. Или можно сделать итератор, генерирующий бесконечную последовательность псевдослучайных чисел. Тоже полезно.
Нет никаких ограничений на next
, он может возвращать всё новые и новые значения, и это нормально.
Разумеется, цикл for..of
по такому итератору тоже будет бесконечным, нужно его прерывать, например, через break
.
Встроенные итераторы
Встроенные в JavaScript итераторы можно получить и явным образом, без for..of
, прямым вызовом Symbol.iterator
.
Например, этот код получает итератор для строки и вызывает его полностью «вручную»:
'use strict';
let str = "Hello";
// Делает то же, что и
// for (var letter of str) alert(letter);
let iterator = str[Symbol.iterator]();
while(true) {
let result = iterator.next();
if (result.done) break;
alert(result.value); // Выведет все буквы по очереди
}
То же самое будет работать и для массивов.
Итого
- Итератор – объект, предназначенный для перебора другого объекта.
- У итератора должен быть метод
next()
, возвращающий объект{done: Boolean, value: any}
, гдеvalue
– очередное значение, аdone: true
в конце. - Метод
Symbol.iterator
предназначен для получения итератора из объекта. Циклfor..of
делает это автоматически, но можно и вызвать его напрямую. - В современном стандарте есть много мест, где вместо массива используются более абстрактные «итерируемые» (со свойством
Symbol.iterator
) объекты, например оператор spread...
. - Встроенные объекты, такие как массивы и строки, являются итерируемыми, в соответствии с описанным выше.