Итераторы в JavaScript

21.06.2018
Смотрите видео
на Youtube

Концепция итерируемости (iterable) заключается в следующем:

С одной стороны, есть набор источников данных, например, строки, массивы, коллекции Map и т.д. С другой стороны, есть потребители данных: языковые конструкции, которые могут получать элементы данных из источников. Пример потребителя — цикл for ... of.

Потребители должны иметь возможность работать с любым источником, не задумываясь о его внутренней реализации. Так с помощью цикла for ... of мы можем перебрать не только элементы массива, но и символы в строке. Для этого в ECMAScript 6 и вводится интерфейс Iterable, который должна реализовывать любая итерируемая коллекция.

Чтобы было понятнее, давайте разбираться на примерах. Допустим у нас есть массив.

const arr = [1, 18, 42];

console.log(arr);
Итерируемая структура данных должна иметь метод с ключом Symbol.iterator, который бы возвращал так называемый итератор. В прототипе есть метод Symbol.iterator, этот метод возвращает итератор. Давайте посмотрим, что это такое. Получим итератор и выведем его в консоль.

const iter = arr[Symbol.iterator]();

console.log(iter);

Итератор — это объект, который имеет метод next(). Метод next() при каждом вызове возвращает следующий элемент из коллекции.

Только это не само значение, а структура IteratorResult — объект, в поле value которого хранится значение элемента, а в поле done хранится признак, дошли мы до конца или нет.

Каждый раз вызывая next(), мы будем получать следующий элемент, пока не дойдём до конца.

Если больше элементов нет, итератор возвращает вот такой объект: в поле value — undefined, а в поле done — true.

Раз массив является итерируемым, мы можем перебрать его элементы с помощью цикла for...of:


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

Строки тоже являются итерируемыми, поэтому цикл for...of прекрасно работает и со строками:


for (let c of 'hello') {
    console.log(c);
}

Синтаксис деструктуризации и spread-оператор тоже используют итераторы, поэтому их можно использовать с любой итерируемой структурой данных, например, со строками:


const [a, b, ...rest] = 'hello';
console.log(a, b, rest);

Здесь мы используем деструктуризацию, чтобы получить первые два символа строки и spread-оператор, чтобы получить остальные в виде массива.

В этом как раз и проявляется прелесть итераторов. Разные языковые конструкции работают с разными структурами данных, например, со строками и массивами, по одному и тому же принципу, не задумываясь об их внутренней реализации.

Но самое на мой взгляд во всём этом интересное - это то, что мы можем написать собственную структуру данных, реализовать интерфейс Iterable и работать с этой структурой с помощью всё тех же конструкций for...of, spread-оператора, Array.from() и т.д.

Давайте рассмотрим пример.

Допустим у нас есть список фильмов, сгруппированный по жанру и мы хотим вывести этот список. Мы можем добавить метод getAllMovies(), который будет возвращать массив всех фильмов и дальше перебрать его и вывести элементы:


var movies = {
  action: [
    'Тёмный рыцарь',
    'Мстители: Война бесконечности',
    'Начало',
    'Матрица'
  ],
  comedy: [
    'Жизнь прекрасна',
    'Огни большого города',
    'Тайна Коко'
  ],
  drama: [
    'Побег из Шоушенка',
    'Крёстный отец',
    'Криминальное чтиво',
    'Список Шиндлера'
  ],
  getAllMovies() {
    return [...this.action, ...this.comedy, ...this.drama];
  }
};


const moviesList = movies.getAllMovies();
for (let movie of moviesList) {
  console.log(movie);
}

А можно сделать структуру данных итерируемой. Для этого нужно добавить метод Symbol.iterator:


var movies = {
  action: [
    'Тёмный рыцарь',
    'Мстители: Война бесконечности',
    'Начало',
    'Матрица'
  ],
  comedy: [
    'Жизнь прекрасна',
    'Огни большого города',
    'Тайна Коко'
  ],
  drama: [
    'Побег из Шоушенка',
    'Крёстный отец',
    'Криминальное чтиво',
    'Список Шиндлера'
  ],
  [Symbol.iterator]() {
    const genres = Object.values(this);

    let index = 0;
    let genreIndex = 0;

    return {
      next() {
        if (index === genres[genreIndex].length) {
          index = 0;
          genreIndex++;
        }

        if (genreIndex === genres.length) {
          return { value: undefined, done: true };
        }

        return {
          value: genres[genreIndex][index++],
          done: false
        };
      }
    };
  }  
};


for (let movie of movies) {
  console.log(movie);
}

В этом примере, если мы дошли до последнего элемента в текущем жанре, увеличиваем индекс жанра и обнуляем индекс фильма. Если это последний жанр, возвращает результат с done = true. А если не последний — возвращаем элемент и увеличиваем индекс на единицу. Теперь мы можем вывести элементы с помощью цикла for...of.

Постой, во втором примере явно больше кода, чем в предыдущем, какой-то итератор — зачем всё это нужно?

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