Как язык, JavaScript построен на концепции итерации. Возможность повторять задачу или действие над набором данных является фундаментальной для многих операций, которые мы выполняем в программировании. В JavaScript концепция итерации реализуется с помощью концепции итерируемых объектов.

Итерируемые объекты — это структуры данных, которые позволяют другим потребителям данных осуществлять последовательный доступ к своим элементам. Например, структура данных, которая выгружает данные один за другим по порядку при помещении в цикл for...of.

Итерируемый протокол состоит из итерируемого объекта, который является самой структурой данных, и итератора, указателя, который перемещается по итерируемому объекту. Когда массив помещается в цикл for...of, его итерируемое свойство с именем Symbol.iterator возвращает итератор. Этот объект можно использовать в общем интерфейсе, используемом всеми структурами управления циклом.

Перебор массивов

Одно из наиболее распространенных применений итерируемых объектов в JavaScript — перебор массивов. Массивы — это наборы данных, которые можно индексировать числовым индексом. В следующем примере показано, как перебирать массив чисел с помощью цикла for...of:

const numbers = [1, 2, 3, 4, 5];

for (const number of numbers) {
  console.log(number);
}

В этом примере цикл for...of перебирает массив numbers, присваивая каждый элемент по очереди переменной number. Затем цикл записывает каждое число в консоль.

Перебор строк

Строки — еще одна распространенная итерация в JavaScript. Строку можно рассматривать как массив символов. В следующем примере показано, как перебирать строку с помощью цикла for...of:

const str = "hello";

for (const char of str) {
  console.log(char);
}

В этом примере цикл for...of перебирает строку str, по очереди присваивая каждый символ переменной char. Затем цикл выводит каждый символ на консоль.

Перебор наборов

Множества — это относительно новая структура данных в JavaScript, добавленная в ECMAScript 6. Множества похожи на массивы тем, что представляют собой наборы данных, но имеют некоторые важные отличия. Одно из этих отличий состоит в том, что наборы не допускают дублирования. В следующем примере показано, как перебирать набор с помощью цикла for...of:

const mySet = new Set([1, 2, 3]);

for (const value of mySet) {
  console.log(value);
}

В этом примере цикл for...of перебирает набор mySet, по очереди присваивая каждое значение переменной value. Затем цикл записывает каждое значение в консоль.

Создание пользовательских итераций

Хотя многие встроенные структуры данных JavaScript являются итерируемыми, также можно создавать собственные итерируемые объекты. Чтобы создать пользовательскую итерацию, объект должен реализовать метод Symbol.iterator. Этот метод должен возвращать объект итератора, предоставляющий метод next().

В следующем примере показано, как создать пользовательскую итерацию, которая генерирует последовательность Фибоначчи:

const fibonacci = {
  [Symbol.iterator]() {
    let a = 0, b = 1;
    return {
      next() {
        const value = a;
        a = b;
        b = value + b;
        return { value, done: false };
      }
    }
  }
}

for (const number of fibonacci) {
  console.log(number);
  if (number > 1000) {
    break;
  }
}

В этом примере объект fibonacci представляет собой пользовательскую итерацию, которая генерирует последовательность Фибоначчи. Метод Symbol.iterator возвращает объект итератора, который генерирует последовательность. Метод next() объекта итератора возвращает следующее значение в последовательности при каждом вызове.

Символ.iterator

Мы можем легко понять концепцию итерируемых объектов, создав свою собственную.

Например, у нас есть объект, который не является массивом, но выглядит подходящим для for..of.

Подобно объекту range, представляющему интервал чисел:

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

Чтобы сделать объект range итерируемым (и, таким образом, позволить for..of работать), нам нужно добавить метод к объекту с именем Symbol.iterator (специальный встроенный символ именно для этого).

  1. Когда for..of запускается, он вызывает этот метод один раз (или ошибки, если не найден). Метод должен возвращать итератор — объект с методом next.
  2. Далее for..of работает только с этим возвращенным объектом.
  3. Когда for..of требуется следующее значение, он вызывает next() для этого объекта.
  4. Результат next() должен иметь вид {done: Boolean, value: any}, где done=true означает, что цикл завершен, иначе value является следующим значением.

Вот полная реализация range с примечаниями:

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

// 1. call to for..of initially calls this
range[Symbol.iterator] = function() {

  // ...it returns the iterator object:
  // 2. Onward, for..of works only with the iterator object below, asking it for next values
  return {
    current: this.from,
    last: this.to,

    // 3. next() is called on each iteration by the for..of loop
    next() {
      // 4. it should return the value as an object {done:.., value :...}
      if (this.current <= this.last) {
        return { done: false, value: this.current++ };
      } else {
        return { done: true };
      }
    }
  };
};

// now it works!
for (let num of range) {
  alert(num); // 1, then 2, 3, 4, 5

Обратите внимание на основную особенность итерируемых объектов: разделение задач.

  • Сам range не имеет метода next().
  • Вместо этого вызовом range[Symbol.iterator]() создается другой объект, так называемый «итератор», и его next() генерирует значения для итерации.

Таким образом, объект итератора отделен от объекта, по которому он выполняет итерацию.