Это короткая, но достаточно полезная статья для продолжающих разработчиков о итераторах в Javascript.
Прежде чем узнаем за итераторы в js, вспомним о том, что такое Symbol:
Symbol — это уникальный и иммутабельный идентификатор. Создается с помощью функции Symbol(), также может иметь метку Symbol('foo'). Символы с одинаковыми метками не равны друг другу, и вообще, любые символы не равны между собой (помним про уникальность).
Существуют системные символы, такие как Symbol.iterator , Symbol.toPrimitive и другие. Системные символы используются самим языком, но мы также можем применять их, чтобы изменять дефолтное поведение некоторых объектов.
Символы являются частью спецификации es6, поэтому не поддерживаются в ie, совсем (caniuse).
Про Symbol.iterator
В основном этот символ используется языком в цикле for…of при переборе свойств объекта. Так же его можно использовать напрямую со встроенными типами данных:
const rangeIterator = '0123456789'[Symbol.iterator]();
console.log(rangeIterator.next()); // {value: "0", done: false}
console.log(rangeIterator.next()); // {value: "1", done: false}
console.log(rangeIterator.next()); // {value: "2", done: false}
...
console.log(rangeIterator.next()); // {value: "9", done: false}
console.log(rangeIterator.next()); // {done: true}
Данный пример со строкой работает, так как у String.prototype имеется свой итератор (спека). Список итерируемых типов в js: String, Array, TypedArray, Map, Set.
Кроме цикла, javascript использует Symbol.iterator в следующих конструкциях: spread operator, yield, destructuring assignment*.
При вызове [Symbol.iterator]() возвращается интерфейс итератора, который выглядит так:
Iterator {
next(); // возврат следующего значения
}
Метод iterator.next() подготавливает (дальше посмотрим как) и возвращает объект вида:
{
value - значение, если есть
done - признак завершенности итераций
}
Применение Symbol.iterator в своих структурах
В качестве примера создадим свою структуру, которую можно проитерировать с помощью for…of и пройтись .next() методом итератора. Посмотрим на применение Symbol.iterator с упомянутыми выше конструкциями языка.
Представим что у нас есть маршрут, проложенный через несколько станций, и мы хотим пройти по маршруту и что-то сделать с каждой станцией, например, вывести в консоли.
Создадим класс Route:
class Route {
stations; // список станций на этом маршруте
iterator; // доступ до итератора
constructor(stations) {
this.stations = stations;
}
// метод получения станции по id
get(idx) {
return this.stations[idx];
}
// реализация итератора
[Symbol.iterator]() {
this.iterator = new RouteIterator(this);
return this.iterator; // разберем ниже
}
}
Как вы можете заметить, наш Route реализует метод Symbol.iterator, таким образом Route является итерируемой сущностью (спека), это означает мы можем пройтись по нему используя for…of (после того как посмотрим реализацию RouteIterator).
Метод [Symbol.iterator]() будет вызван столько раз, сколько обращений к нему было. То есть, если несколько циклов друг за другом пытаются пройтись по route, то на каждый цикл будет вызван [Symbol.iterator](), поэтому для каждого вызова мы создаем новый экземпляр RouteIterator.
Теперь познакомимся с самим RouteIterator. Это класс реализующий интерфейс итератора для Route сущности. Посмотрим на него:
class RouteIterator {
_route; // доступ до итерируемого объекта
_nextIdx; // указатель следующего значения
constructor(route) {
this._route = route;
this._nextIdx = 0;
}
next() {
if (this._nextIdx === this._route.stations.length) {
return { done: true } // проверка на последний элемент
}
const result = {
value: this._route.get(this._nextIdx),
done: false
}
this._nextIdx++;
return result;
}
}
В данном классе мы имеем доступ до итерируемой коллекции (свойство route), так же nextIdx - это указатель на следующее значение в нашей коллекции.
Метод next() первым делом проверяет не завершился ли маршрут, и если завершился - возвращает что итерации завершены. Иначе мы берем следующее значение в коллекции route, говорим, что итерации не завершены, перемещаем указатель и возвращаем результат.
Теперь мы можем пройтись по коллекции route через for…of:
for (let item of route) {
console.log(item);
}
Такой код выведет список станций, который мы передали в Route.
Теперь пройдемся по станциям с помощью .next() метода итератора:
console.log(route.iterator.next()) // {value: Москва, done: false}
console.log(route.iterator.next()) // {value: Питер, done: false}
console.log(route.iterator.next()) // {value: Казань, done: false}
console.log(route.iterator.next()) // {done: true}
Но лучше делать это используя функции генераторы:
function* gen() { yield* route; }
const g = gen();
g.next() // {value: "Москва", done: false}
g.next() // {value: "Питер", done: false}
g.next() // {value: "Казань", done: false}
g.next() // {value: undefined, done: true}
Symbol.iterator используется при деструктуризации:
const [a, b, c] = route;
// a - "Москва"
// b - "Питер"
// с - "Казань"
и со spread оператором:
function test(a, b, c) { console.log(a, b, c) }
test(…route) // "Москва" "Питер" "Казань"
Результаты
Создали свой класс, сделали его итерируемым и использовали с конструкциями javascript'a. Спасибо за внимание =).
Материалы
Невозможно полностью освоить новый материал только одной статьей, поэтому вот несколько дополнительных:
Про паттерн итератор из книги Рефакторинг Гуру
Про Symbol из книги Ильи Кантора и на MDN
Автор: Ильгам Габдуллин