Итераторы представляют абстракцию для перебора наборов данных и применяются для организации последовательного доступа к элементам наборов данных - массивам, объектам Set, Map, строкам и т.д..Так, благодаря итераторам мы можем перебрать набор данных (например, массив) с помощью цикла for-of:
const people = ["Tom", "Bob", "Sam"];
for(const person of people){
console.log(person);
}
В цикле for-of справа от оператора of указывается набор данных или перебираемый объект (то, что назвается Iterable), из которого в цикле мы можем получить отдельные элементы. Но эта возможность перебора некоторого объекта, как, например, массива в примере выше, реализуются благодаря тому, что эти объекты применяют итераторы. Рассмотрим подробнее, что представляют итераторы и как можно создать свой итератор.
Любой итерируемый объект (например, массив, Map, Set и т.д.) хранит в свойстве Symbol.iterator функцию, которая возвращает связанный с объектом итератор:
const people = ["Tom", "Bob", "Sam"];
// получаем итератор массива
const iterator = people[Symbol.iterator]();
console.log(iterator); // Array Iterator {}
Здесь получаем итератор массива, поэтому на консоль будет выведено что-то наподобие Array Iterator {}
Другой пример - строка тоже представляет перебираемый объект, которую можно перебрать посимвольно:
const username = "Tom";
for(char of username){
console.log(char);
}
Соответственно для строки мы тоже можем получить итератор:
const username = "Tom";
// получаем итератор строки
const iterator = username[Symbol.iterator]();
console.log(iterator); // StringIterator {}
Итератор строки представляет тип StringIterator. Аналогичным образом можно получать итераторы и для других типов перебираемых объектов.
Стоит отметить, что у различных типов могут быть различные дополнительные методы для получения итератора. Например, у массивов есть метод entries(), который также возвращает итератор массива:
const people = ["Tom", "Bob", "Sam"];
console.log(people.entries()); // Array Iterator {}
Итераторы предоставляют метод next(), который возвращает объект с двумя свойствами: value и done
{value, done}
Свойство value хранит собственно значение текущего перебираемого элемента. А свойство done
указывает, есть ли еще в коллекции объекты, доступные для перебора. Если в наборе еще есть элементы, то свойство done равно false
Если же доступных элементов для перебора больше нет, то это свойство равно true, а метод next() возвращает объект
{done: true}
Например:
const people = ["Tom", "Bob", "Sam"];
const iter = people[Symbol.iterator]();
const result = iter.next();
console.log(result); // {value: "Tom", done: false}
В данном случае вызываем метод next() и получаем из итератора первыый результат:
{value: "Tom", done: false}
Здесь мы видим, что текущий объект представляет строку "Tom", а значение done: false указывает, что в массиве еще есть элементы для перебора.
Мы можем последовательно несколько раз вызвать метод next() для получения других элементов массива:
const people = ["Tom", "Bob", "Sam"];
const iter = people[Symbol.iterator]();
console.log(iter.next()); // {value: "Tom", done: false}
console.log(iter.next()); // {value: "Bob", done: false}
console.log(iter.next()); // {value: "Sam", done: false}
console.log(iter.next()); // {value: undefined, done: true}
Консольный вывод программы:
{value: "Tom", done: false}
{value: "Bob", done: false}
{value: "Sam", done: false}
{value: undefined, done: true}
Здесь мы видим, что при каждом новом вызове метода next() мы получаем из массива следующий объект. А когда объектов для перебора
больше не останется, то свойство done будет равно true.
Используя метод next(), мы сами можем перебрать все объекты массива:
const people = ["Tom", "Bob", "Sam"];
const iter = people[Symbol.iterator]();
while(!(item = iter.next()).done){
console.log(item.value);
}
Здесь в цикле while из метода next() итератора получаем текущий объект в переменную item: item = items.next()
И смотрим на ее свойство done - если оно равно false (то есть в наборе еще есть элементы), то продолжаем цикл
while(!(item = iter.next()).done){
В цикле обращаемся к свойству value полученного объекта
console.log(item.value);
Консольный вывод:
Tom Bob Sam
Но в этом нет смысла, поскольку все коллекции, которые возвращают итераторы, поддерживают перебор с помощью цикла for...of, который как раз и использует итератор для получения элементов.
Для примера реализуем итератор, который перебирает массив с конца:
const people = ["Tom", "Bob", "Sam"];
function reverseArrayIterator(array) {
let count = array.length;
return {
next: function(){
if (count > 0) {
return {
value: array[--count],
done: false
};
}
else {
return {
value: undefined,
done: true
};
}
}
}
};
const iter = reverseArrayIterator(people);
while(!(item = iter.next()).done){
console.log(item.value);
}
Здесь сначала инициализируется переменная count, которая количество перебранных элементов массива. Первоначально переменная имеет значение, равное длине массива.
Далее функция возвращает объект итератора. Его метод next() реализует поведение итерации: если счетчик count
больше 0 (то есть имеются еще элементы для перебора), то next() возвращает объект, свойство done которого имеет значение false
(поскольку итератор еще не достиг конца или точнее начала массива), а свойство value содержит соответствующий элемент из массива, на который указывает
переменная count после декремента.
Когда переменная count станет равна 0 (т. е. итератор достиг конца), next() возвращает объект, у которого
свойство done имеет значение true, а свойство value имеет значение undefined.
Таким образом, мы получим итератор, который перебирает объекты массива с конца. Консольный вывод:
Sam Bob Tom
Однако при выполнении цикла for..of элементы массива по прежнему перебираются с начала. Применим наш итератор глобально, чтобы он также использовался в цикле for..of:
const people = ["Tom", "Bob", "Sam"];
function reverseArrayIterator() {
const array = this;
let count = array.length;
return {
next: function(){
if (count > 0) {
return {
value: array[--count],
done: false
};
}
else {
return {
value: undefined,
done: true
};
}
}
}
};
// меняем итератор для массива people
people[Symbol.iterator]=reverseArrayIterator;
for(person of people){
console.log(person);
}
Здесь сделано два ключевых изменения. Во-первых, нам надо внутри итератора получить текущий объект через this:
const array = this;
Созданную функцию итератора надо присвоить свойству Symbol.iterator:
people[Symbol.iterator]=reverseArrayIterator;
Разные объекты могут иметь свою собственную реализацию итератора. И при необходимости мы можем определить объект со своим итератором. Применение итераторов предоставляет нам способ создать объект, который будет вести себя как коллекция элементов
Для создания перебираемого объекта нам надо определить в объекта метод [Symbol.iterator](). Этот метод собственно и будет представлять итератор:
const iterable = {
[Symbol.iterator]() {
return {
next() {
// если еще есть элементы
return { value: ..., done: false };
// если больше нет элементов
return { value: undefined, done: true };
}
};
}
};
Метод [Symbol.iterator]() возвращает объект, который имеет метод next(). Этот метод возвращает объект с двумя
свойствами value и done.
Если в нашем объекте есть элементы, то свойство value содержит собственно значение элемента, а свойство done равно false.
Если доступных элементов больше нет, то свойство done равно true.
Например, реализуем простейший объект с итератором, который возвращает некоторый набор чисел:
const iterable = {
[Symbol.iterator]() {
return {
current: 1,
end: 3,
next() {
if (this.current <= this.end) {
return { value: this.current++, done: false };
}
return { done: true };
}
};
}
};
Здесь итератор фактически возвращает числе от 1 до 3. Для отслеживания текущего элемента в объекте, который возвращается методом ,
определены два свойства:
current: 1, end: 3,
Свойство current собственно хранит значение текущего элемента. А свойство end задает предел. То есть в данном случае итератор возвращает числа от 1 до 3.
В методе next(), если текущее значение меньше или равно редельному значению, возвращаем объект
return { value: this.current++, done: false };
Инкремент this.current++ приведет к тому, что при следующем вызове метода next значение current будет на единицу больше.
Если достигнут предел, то возвращаем объект
return { done: true };
Это будет указывать, что объектов больше нет.
Получим из итератора возвращаемые им элементы:
const myIterator = iterable[Symbol.iterator](); // получаем итератор
console.log(myIterator.next()); // {value: 1, done: false}
console.log(myIterator.next()); // {value: 2, done: false}
console.log(myIterator.next()); // {value: 3, done: false}
console.log(myIterator.next()); // {done: true}
Здесь сначала получаем итератор в константу myIterator. Затем при обращении к ее методу next() последовательно получаем
все элементы. При четвертом вызове метода next условный перебор элементов в итераторе закончен, и метод возвращает объект {done: true}.
Однако если мы хотим перебрать наш объект и получить из него его элементы, то нам не надо обращаться к методу next(). Поскольку объект iterable реализует
итератор, то его можно перебрать с помощью цикла for-of:
const iterable = {
[Symbol.iterator]() {
return {
current: 1,
end: 3,
next() {
if (this.current <= this.end) {
return { value: this.current++, done: false };
}
return { done: true };
}
};
}
};
for (const value of iterable) {
console.log(value);
}
Консольный вывод:
1 2 3
Цикл for-of автоматически обращается к методу next() и извлекает значение.
Рассмотрим еще один пример:
// объект-компания
const company = {
// массив работников
employees: [
{name: "Tom", age: 39, position: "Senior Developer"},
{name: "Bob", age: 43, position: "Middle Developer"},
{name: "Sam", age: 28, position: "Junior Developer"},
]
};
// устанавливаем итератор
company[Symbol.iterator] = function() {
const array = this.employees; // получаем массив работников
let current = 0;
return {
next() {
if (current < array.length) {
return { value: array[current++].name, done: false };
}
return { value:undefined, done: true };
}
};
};
for (const employee of company) {
console.log(employee);
}
Здесь объект company представляет условную компанию, в которой есть массив работников - массив employee. Допустим, с помощью итератора мы хотим получать
имя каждого работника. Для этого для объекта company устанавливаем функцию итератора, которая перебирает все элементы из массива employees. Консольный
вывод программы:
Tom Bob Sam