ES-2015: Итераторы

В современный JavaScript добавлена новая концепция «итерируемых» (iterable) объектов.

Итерируемые или, иными словами, «перебираемые» объекты – это те, содержимое которых можно перебрать в цикле.

Например, перебираемым объектом является массив. Но не только он. В браузере существует множество объектов, которые не являются массивами, но содержимое которых можно перебрать (к примеру, список DOM-узлов).

Для перебора таких объектов добавлен новый синтаксис цикла: for..of.

Например:

'use strict';

let arr = [1, 2, 3]; // массив — пример итерируемого объекта

for (let value of arr) {
  alert(value); // 1, затем 2, затем 3
}

Также итерируемой является строка:

'use strict';

for (let char of "Привет") {
  alert(char); // Выведет по одной букве: П, р, и, в, е, т
}

Итераторы – расширяющая понятие «массив» концепция, которая пронизывает современный стандарт JavaScript сверху донизу.

Практически везде, где нужен перебор, он осуществляется через итераторы. Это включает в себя не только строки, массивы, но и вызов функции с оператором spread f(...args), и многое другое.

В отличие от массивов, «перебираемые» объекты могут не иметь «длины» length. Как мы увидим далее, итераторы дают возможность сделать «перебираемыми» любые объекты.

Свой итератор

Допустим, у нас есть некий объект, который надо «умным способом» перебрать.

Например, range – диапазон чисел от from до to, и мы хотим, чтобы for (let num of range) «перебирал» этот объект. При этом под перебором мы подразумеваем перечисление чисел от from до to.

Объект range без итератора:

let range = {
  from: 1,
  to: 5
};

// хотим сделать перебор
// for (let num of range) ...

Для возможности использовать объект в for..of нужно создать в нём свойство с названием Symbol.iterator (системный символ).

При вызове метода Symbol.iterator перебираемый объект должен возвращать другой объект («итератор»), который умеет осуществлять перебор.

По стандарту у такого объекта должен быть метод next(), который при каждом вызове возвращает очередное значение и окончен ли перебор.

В коде это выглядит следующим образом:

'use strict';

let range = {
  from: 1,
  to: 5
}

// сделаем объект range итерируемым
range[Symbol.iterator] = function() {

  let current = this.from;
  let last = this.to;

  // метод должен вернуть объект с методом next()
  return {
    next() {
      if (current <= last) {
        return {
          done: false,
          value: current++
        };
      } else {
        return {
          done: true
        };
      }
    }

  }
};

for (let num of range) {
  alert(num); // 1, затем 2, 3, 4, 5
}

Как видно из кода выше, здесь имеет место разделение сущностей:

  • Перебираемый объект range сам не реализует методы для своего перебора.
  • Для этого создаётся другой объект, который хранит текущее состояние перебора и возвращает значение. Этот объект называется итератором и возвращается при вызове метода range[Symbol.iterator].
  • У итератора должен быть метод next(), который при каждом вызове возвращает объект со свойствами:
    • value – очередное значение,
    • done – равно false если есть ещё значения, и true – в конце.

Конструкция for..of в начале своего выполнения автоматически вызывает Symbol.iterator(), получает итератор и далее вызывает метод next() до получения done: true. Такова внутренняя механика. Внешний код при переборе через for..of видит только значения.

Такое отделение функционала перебора от самого объекта даёт дополнительную гибкость. Например, объект может возвращать разные итераторы в зависимости от своего настроения и времени суток. Однако, бывают ситуации когда оно не нужно.

Если функционал по перебору (метод next) предоставляется самим объектом, то можно вернуть this в качестве итератора:

'use strict';

let range = {
  from: 1,
  to: 5,

  [Symbol.iterator]() {
    return this;
  },

  next() {
    if (this.current === undefined) {
      // инициализация состояния итерации
      this.current = this.from;
    }

    if (this.current <= this.to) {
      return {
        done: false,
        value: this.current++
      };
    } else {
      // очистка текущей итерации
      delete this.current;
      return {
        done: true
      };
    }
  }

};

for (let num of range) {
  alert(num); // 1, затем 2, 3, 4, 5
}

// Произойдёт вызов Math.max(1,2,3,4,5);
alert( Math.max(...range) ); // 5 (*)

При таком подходе сам объект и хранит состояние итерации (текущий перебираемый элемент).

В данном случае это работает, но для большей гибкости и понятности кода рекомендуется, всё же, выделять итератор в отдельный объект со своим состоянием и кодом.

Оператор spread ... и итераторы

В последней строке (*) примера выше можно видеть, что итерируемый объект передаётся через spread для Math.max.

При этом ...range автоматически превращает итерируемый объект в массив. То есть произойдёт цикл for..of по range, и его результаты будут использованы в качестве списка аргументов.

Бесконечные итераторы

Возможны и бесконечные итераторы. Например, пример выше при range.to = Infinity будет таковым. Или можно сделать итератор, генерирующий бесконечную последовательность псевдослучайных чисел. Тоже полезно.

Нет никаких ограничений на next, он может возвращать всё новые и новые значения, и это нормально.

Разумеется, цикл for..of по такому итератору тоже будет бесконечным, нужно его прерывать, например, через break.

Встроенные итераторы

Встроенные в JavaScript итераторы можно получить и явным образом, без for..of, прямым вызовом Symbol.iterator.

Например, этот код получает итератор для строки и вызывает его полностью «вручную»:

'use strict';

let str = "Hello";

// Делает то же, что и
// for (var letter of str) alert(letter);

let iterator = str[Symbol.iterator]();

while(true) {
  let result = iterator.next();
  if (result.done) break;
  alert(result.value); // Выведет все буквы по очереди
}

То же самое будет работать и для массивов.

Итого

  • Итератор – объект, предназначенный для перебора другого объекта.
  • У итератора должен быть метод next(), возвращающий объект {done: Boolean, value: any}, где value – очередное значение, а done: true в конце.
  • Метод Symbol.iterator предназначен для получения итератора из объекта. Цикл for..of делает это автоматически, но можно и вызвать его напрямую.
  • В современном стандарте есть много мест, где вместо массива используются более абстрактные «итерируемые» (со свойством Symbol.iterator) объекты, например оператор spread ....
  • Встроенные объекты, такие как массивы и строки, являются итерируемыми, в соответствии с описанным выше.