Это короткая, но достаточно полезная статья для продолжающих разработчиков о итераторах в 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

Комментарии (7)


  1. DmitryKoterov
    23.12.2019 00:53
    +2

    Еще три копейки про итераторы:


    1. Там не только next(), но еще методы return() и throw(), в которых кроется весь «дьявол в деталях», когда идет речь про досрочное прерывание итерации, например.
    2. В конце из next() возвращается не просто { done: true }, а { done: true, value: xyz } где xyz — это то, что генератор вернул через оператор return. И это значение xyz не учитывается при, например, проходе по for-of, spread и т.д. Также next() возвращает этот самый xyz только один раз, дальше будет undefined.
    3. В статье зачем-то созданный итератор присваивается в this.iterator, не надо так делать.


    1. ilgamgabdullin Автор
      23.12.2019 11:52

      Добрый день!
      Благодарю за полезный комментарий)
      Статью поправил


    1. faiwer
      23.12.2019 22:08

      Вначале не поверил пункту 2. Однако:


      function * gen(){ yield 1; yield 2; return 3; };
      iter = gen();
      [...iter]; // [1,2]

      всё так


  1. MKMatriX
    23.12.2019 09:26

    «for… of» и "..." перегрузили, 2ality.com/2011/12/fake-operator-overloading.html, а вот еще одна старая статья про ограниченные возможности перегрузки других операторов.

    Не то чтобы те методы совсем хороши, но дают возможности для
    var p = new Point();
    p._ = new Point(1, 2) + new Point(3, 4) + new Point(5, 6);
    p._ = new Point(1, 2) * new Point(3, 4) * new Point(5, 6);


    1. limitofzero
      23.12.2019 12:11

      Прошу прощения, но причем тут перегрузка? For of это отличный пример полиморфизма, а не перегрузки. В каком месте for of перегружается? For of конструкция работает только с итерируемыми объектами.


      1. MKMatriX
        23.12.2019 12:34

        Может я немного не точен в определениях, с института много времени прошло. Просто прочитав статью первым делом написал небольшой кусочек когда меняющий поведение Array.prototype[Symbol.iterator], так чтобы при использовании спреда получить массив длинной в десять со случайными элементами.

        Ваш пример конечно явный полиморфизм, но мне была интересна возможность использования именно перегрузки, поскольку это повышает гибкость, учитывая весьма ограниченные методы работы с массивами в js, было весьма полезно)


        1. ilgamgabdullin Автор
          23.12.2019 13:25

          Добрый день!)
          Спасибо за ваш комментарий!
          Array.prototype[Symbol.iterator] действительно переписывает существующую в Array.prototype реализацию итератора, но в статье мы так не делаем =)

          В данной статье я хотел показать как мы можем написать свою сущность которая реализует Symbol.iterator, так же как javascript в Array.prototype, String.prototype и так далее
          + показать как использовать конструкции языка для взаимодействия с нашей сущностью.

          Как заметил limitofzero, это действительно хороший пример полиморфизма)