Итераторы
Мы хотим сделать этот проект с открытым исходным кодом доступным для людей во всем мире. Пожалуйста, помогите нам перевести это руководство на другие языки.
18 февраля 2020 г.

Итераторы

Материал на этой странице устарел, поэтому скрыт из оглавления сайта.

Более новая информация по этой теме находится на странице https://learn.javascript.ru/iterable.

В современный 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 ....
  • Встроенные объекты, такие как массивы и строки, являются итерируемыми, в соответствии с описанным выше.
Карта учебника