Итераторы представляют абстракцию для перебора наборов данных и применяются для организации последовательного доступа к элементам наборов данных - массивам, объектам 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