Итераторы
Итераторы применяются для организации последовательного доступа к элементам наборов данных - массивам
, объектам Set
, объектам Map
, строкам
, благодаря итераторам мы можем перебрать набор данных с помощью цикла for-of
:
const people = ["Tom", "Bob", "Sam"];
for(const person of people){
console.log(person);
}
В цикле for-of
справа от оператора of
указывается набор данных или перебираемый объект из которого в цикле мы можем получить отдельные элементы. Но эта возможность перебора некоторого объекта, как например массива в примере выше, реализуются благодаря тому, что эти объекты применяют итераторы. Рассмотрим подробнее, что представляют итераторы и как можно создать свой итератор.
Итераторы предоставляют метод next()
, который возвращает объект с двумя свойствами: value
и done
:
{value, done}
Свойство value
хранит собственно значение текущего перебираемого элемента, свойство done
указывает, есть ли еще в коллекции объекты доступные для перебора. Если в наборе еще есть элементы, то свойство done
равно false
. Если доступных элементов для перебора больше нет, то это свойство равно true
, а метод next()
возвращает объект.
{done: true}
Некоторые типы в JavaScript имеют методы которые возвращают итераторы. Например, метод entries()
типа Array
:
const people = ["Tom", "Bob", "Sam"];
const items = people.entries();
console.log(items.next());
Здесь метод next()
возвратит следующий объект на консоль:
{value: Array(2), done: false}
done: false
value: Array(2)
0: 0
1: "Tom"
length: 2
__proto__: Array(0)
__proto__: Object
Здесь мы видим, что свойство done имеет значение false
, так как мы перебрали только один элемент в множестве, и там еще есть два элемента. Свойство value
представляет массив из двух значений. Первое значение представляет ключ или индекс элемента массива, а второй элемент - значение по этому индексу.
Соответственно мы можем организовать и перебор всей коллекции:
const people = ["Tom", "Bob", "Sam"];
// получаем итератор
const items = people.entries();
while(!(item = items.next()).done){
console.log(item.value);
}
Здесь в цикле while
из метода next()
итератора получаем текущий объект в переменную item
: item = items.next()
. Смотрим на ее свойство done
- если оно равно false
, то есть в наборе еще есть элементы, продолжаем цикл:
while(!(item = items.next()).done){
В цикле обращаемся к свойству value
полученного объекта:
console.log(item.value);
Консольный вывод:
[0, "Tom"]
[1, "Bob"]
[2, "Sam"]
Поскольку каждый возвращаемый элемент представляет массив, где первый элемент это индекс в массиве, а второй это само значение, тогда обратившись ко второму элементу, мы можем получить значение:
while(!(item = items.next()).done){
console.log(item.value[1]);
}
В этом нет смысла, поскольку все коллекции которые возвращают итераторы, поддерживают перебор с помощью цикла for...of
, который как раз и использует итератор для получения элементов.
Разные объекты могут иметь свою собственную реализацию итератора. И при необходимости мы можем определить объект со своим итератором. Применение итераторов предоставляет нам способ создать объект, который будет вести себя как коллекция элементов.
Для создания перебираемого объекта нам надо определить в объекта метод [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 team = {
[Symbol.iterator]() {
return {
index: 0,
members: ["Tom", "Alice", "Kate"],
next() {
if (this.index < this.members.length) {
return { value: this.members[this.index++], done: false };
}
return { done: true };
}
};
}
};
for (const member of team) {
console.log(member);
}
Здесь объект team
представляет условную команду и применяет итератор, который возвращает элементы из массива members
- участники команды. Для отслеживания индекса текущего возвращаемого элемента из массива members
определено свойство index
, которое инкрементируется при каждом вызове метода next
.
Реализация итератора позволяет рассматривать объект team
как условную коллекцию, которую можно перебрать в цикле for-of
. Консольный вывод программы:
Tom
Alice
Kate