Как стать автором
Обновить

Symbol.iterator в Javascript

Время на прочтение4 мин
Количество просмотров17K

Это короткая, но достаточно полезная статья для продолжающих разработчиков о итераторах в Javascript.


image


Прежде чем узнаем за итераторы в 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(); // возврат следующего значения

 return(); // опциональный метод
 throw(); // опциональный метод
}

Методы .next(), .return(), .throw() подготавливают (дальше посмотрим как) и возвращают объект вида:


{
 value - значение, если есть
 done - признак завершенности итераций
}

Методы .return() и .throw() используются, например, при преждевременном окончании итерации. Подбробнее о них можно почитать в спеке ecmascript.


Применение Symbol.iterator в своих структурах


В качестве примера создадим свою структуру, которую можно проитерировать с помощью for…of и так же посмотрим на применение Symbol.iterator с упомянутыми выше конструкциями языка.


Представим что у нас есть маршрут, проложенный через несколько станций, и мы хотим пройти по маршруту и что-то сделать с каждой станцией, например, вывести в консоли. 


Создадим класс Route:


class Route {
 stations; // список станций на этом маршруте

 constructor(stations) {
   this.stations = stations;
 }

 // метод получения станции по id
 get(idx) {
   return this.stations[idx];
 }

 // реализация итератора
 [Symbol.iterator]() {
   return new RouteIterator(this); // разберем ниже
 }
}

Как вы можете заметить, наш 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:


const route = new Route(['Москва', 'Питер', 'Казань'])

for (let item of route) {
 console.log(item);
}

Такой код выведет список станций, который мы передали в Route.


Теперь пройдемся по станциям используя функции генераторы:


function* gen() { 
  yield* route; 
  return 'x'; // возвращается после завершения итерации на вызов следующего .next()
}
const g = gen();
g.next() // {value: "Москва", done: false}
g.next() // {value: "Питер", done: false}
g.next() // {value: "Казань", done: false}
g.next() // {value: 'x', done: true}
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

Теги:
Хабы:
Всего голосов 10: ↑9 и ↓1+8
Комментарии7

Публикации

Истории

Работа

Ближайшие события