Подписывайтесь на мой твиттер, там всегда что-нибудь интересное!

Самое понятное руководство по итераторам в ES6 JavaScript. С примерами

В этой статье мы попытаемся понять и проанализировать итераторы в JS. Это сравнительно новые способы для того, чтобы пройтись по любой коллекции данных. Итераторы были представлены в ES6 и стали действительно популярны, так как оказались ну уж очень полезными и могут широко использоваться во многих рабочих кейсах.

Адаптивный перевод статьи — A Simple Guide to ES6 Iterators in JavaScript with Examples

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

Введение

Представим, что у нас есть массив:

const myFavouriteAuthors = [
  'Neal Stephenson',
  'Arthur Clarke',
  'Isaac Asimov', 
  'Robert Heinlein'
];

Рано или поздно, вам понадобится получить все отдельные значения массива для их: последующего вывода на страницу, манипулирования или для вообще, любого действия связанного с ними. Если я спрошу вас, как вы это всё сделаете? Вы ответите — да легко! Вы просто пустите цикл по всему массиву с помощью forwhilefor-of или одним из этих методов цикла. Пример выполнения был бы таким:

Теперь представьте, что вместо предыдущего массива, у нас есть кастомная структура данных, которая хранит ваших любимых авторов. Как тут:

Теперь myFavoriteAuthors это объект, который содержит другой объект allAuthorsAllAuthors содержит три массива с ключами fictionscienceFiction и fantasy. Теперь, если я попрошу вас пройтись по myFavouriteAuthors, чтобы получить всех авторов, как вы это сделаете? Вы можете попробовать какие-нибудь комбинации циклов, чтобы получить все нужные данные.

Тем не менее, если вы сделаете так, то:

for (let author of myFavouriteAuthors) { 
  console.log(author)
}
// TypeError: {} is not iterable 
// Хоба, а так нельзя!

То вы получите TypeError, говорящую о том, что объект нельзя проитерировать. Давайте посмотрим, что мы можем итерировать и как мы можем сделать сам объект таковым. В конце статьи вы узнаете как использовать for-of цикл на кастомных объектах, а в нашем случае на myFavoriteAuthors.

Итераторы и итерируемые

Вы уже видели проблему в предыдущем параграфе. Там нет простого способа пройтись по всем авторам заданного кастомного объекта. Нам понадобится какой-нибудь метод с помощью которого мы можем последовательно извлечь все нужные нам данные.

Давайте добавим метод getAllAuthors в myFavouriteAuthors, который отдаст нам всех авторов. Например:

Это довольно простой подход. Он решает нашу задачу в том, чтобы получить всех авторов. Тем не менее, несколько проблем всё таки всплывают в такой имплементации. Некоторые из них:

Имя getAllAuthors очень специфичное. Если кто-то ещё сделает свой myFavouriteAuthors, то они могут назвать его retrieveAllAuthors.

Мы как разработчики, всегда должны знать конкретные методы, которые возвращают все требуемые данные. В нашем случае, это getAllAuthors.

GetAllAuthors возвращает массив строк, состоящий из всех авторов. А что если другой разработчик возвращает массив объектов в таком формате:

[ {name: 'Agatha Christie'}, {name: 'J. K. Rowling'}, ... ]

Разработчику нужно будет точно знать имя и тип метода, которые возвращает все данные.

Что если мы сделаем правило, в котором имя метода и его тип будут фиксированными и неизменными?

Давайте назовем этот метод — iteratorMethod.

Подобный шаг был сделан ECMA, чтобы стандартизировать этот процесс проведения цикла по кастомным объектам. Однако, вместо использования имени iteratorMethod, ECMA использовала имя Symbol.iteratorSymbols предлагают имена, которые уникальны и не могут конфликтовать с именами других свойств. Также, Symbol.iterator вернет объект именуемый iterator. У этого итератора будет метод, под названием next, который возвращает объект с ключами value и done.

Ключ value, будет содержать актуальное значение. Это может быть всё, что угодно. А done — это булево значение(логический тип данных). Он определяет все ли значения были отданы.

Схема нарисованная ниже может помочь с выстраиванием отношений между итерируемыми, итераторами и next. Эти отношения называются Iteration Protocol, то есть протокол итерации.

Согласно книге ExploringJS от Dr Axel Rauschmayer —

Итерируемые — это структура данных, которой нужно сделать свои элементы доступными для вне. Это делается выполнением метода, чей ключ — Symbol.iterator. Этот метод является основой для итераторов. Вот так, он создаст итераторы.

Итератор — это указатель для навигации по элементам структуры данных.

Делаем объекты итерируемыми

Как мы уже узнали в предыдущей секции, нам нужно выполнить метод под названием Symbol.iterator. Мы будем использовать синтаксис вычисления свойств. Короткий пример:

На 4й строке мы сделали итератор. Это объект с методом next. Этот метод возвращает значение в соответствии с переменной step. На строке 25, мы получаем iterator. На 27й, мы вызываем next. И мы продолжаем вызывать next, пока done не станет true.

Это именно то, что происходит в for-of цикле. Циклы for-of берут итерируемое и создают их итератор. Next() же продолжает вызываться до то момента, пока done, не станет true.

Итерируемые в JavaScript

Много всего является итерируемым в JavaScript. Это может быть заметным не сразу, но если вы начнете внимательно вглядываться, то итерируемые начнут постепенно представляться вашему взору.

Вот это всё итерируемые-

Массивы (Arrays) и Типизированные массивы (TypedArrays)

Строки (Strings) — итерируются по каждому символу или кодовому символу в Unicode.

Maps — итерируется по всем парам ключевых значений.

Set — итерируется по всем элементам.

Arguments — массиво-подобная специальная переменная в функциях.

Элементы DOM — тут над этим ещё ведется работа

Другие итерируемые конструкции в JS это —

for-of цикл —для for-of нужны итерируемые элементы. В противном случае, он выкинет ошибку TypeError.

for (const value of iterable) { ... }

Деструктурицизация массивов — деструктуризация происходит из-за итерируемых. Давайте посмотрим как:

const array = ['a', 'b', 'c', 'd', 'e'];
const [first, ,third, ,last] = array;

Это по сути следующий код:

const array = ['a', 'b', 'c', 'd', 'e'];
const iterator = array[Symbol.iterator]();
const first = iterator.next().value
iterator.next().value // Так как оно пропущено, то не назначается
const third = iterator.next().value
iterator.next().value // Так как оно пропущено, то не назначается
const last = iterator.next().value

Оператор расширения spread. Вот код:

const array = ['a', 'b', 'c', 'd', 'e'];
const newArray = [1, ...array, 2, 3];

Его можно записать вот так:

const array = ['a', 'b', 'c', 'd', 'e'];
const iterator = array[Symbol.iterator]();
const newArray = [1];
for (let nextValue = iterator.next(); nextValue.done !== true; nextValue = iterator.next()) {
  newArray.push(nextValue.value);
}
newArray.push(2)
newArray.push(3)

Promise.all и Promise.race принимают итерируемые.

Maps и Set.

Конструктор Map делает итерируемыми[key, value] пары в Map, а конструктор Set делает итерируемыми элементы в Set.

const map = new Map([[1, 'one'], [2, 'two']]);
map.get(1) 
// one
const set = new Set(['a', 'b', 'c]);
set.has('c');
// true

Итераторы также обязательны для понимания функций генераторов

Делаем myFavouriteAuthors итерируемыми

Вот код, который сделает myFavouriteAuthors итерируемым:

const myFavouriteAuthors = {
  allAuthors: {
    fiction: [
      'Agatha Christie', 
      'J. K. Rowling',
      'Dr. Seuss'
    ],
    scienceFiction: [
      'Neal Stephenson',
      'Arthur Clarke',
      'Isaac Asimov', 
      'Robert Heinlein'
    ],
    fantasy: [
      'J. R. R. Tolkien',
      'J. K. Rowling',
      'Terry Pratchett'
    ],
  },
  [Symbol.iterator]() {
    // Получаем все жанры
    const genres = Object.values(this.allAuthors);
    
    // Индекс заданного жанра и автора
    let currentAuthorIndex = 0;
    let currentGenreIndex = 0;
    
    return {
      // Выполнение next()
      next() {
        // Авторы в соответствии с индексом заданного автора
        const authors = genres[currentGenreIndex];
        
        // doNotHaveMoreAuthors будет true, когда закончатся все авторы в массиве.
        // Вот и всё, собственно, всех авторов взяли.
        const doNothaveMoreAuthors = !(currentAuthorIndex < authors.length);
        if (doNothaveMoreAuthors) {
          // Когда это происходит, мы двигаем индекс жанра к следующему жанру
          currentGenreIndex++;
          // И сбрасываем индекс автора на 0, чтобы получить новый набор авторов
          currentAuthorIndex = 0;
        }
        
        // Если мы прошлись по всем жанрам, то нам нужно сказать всем итераторам о том,
        // что мы больше не можем дать значений.
        const doNotHaveMoreGenres = !(currentGenreIndex < genres.length);
        if (doNotHaveMoreGenres) {
          // Следовательно, мы возвращаем done со значением true.
          return {
            value: undefined,
            done: true
          };
        }
        
        // Если всё верно, возвращаем автора из данного жанра 
        // и увеличиваем currentAuthorindex
        // так что в следующий раз, будет отдам уже следующий автор.
        return {
          value: genres[currentGenreIndex][currentAuthorIndex++],
          done: false
        }
      }
    };
  }
};
for (const author of myFavouriteAuthors) {
  console.log(author);
}
console.log(...myFavouriteAuthors)

Со знаниями, полученными в этой статье, вы сможете легко понять то, как работают итераторы. Логика может быть немного сложна. Поэтому тут есть комментарии в коде. Но лучшим способом усвоить и понять эту концепцию, является игра с кодом в бразуере или ноде.