Итеративные объекты и итераторы в JavaScript

JavaScript поддерживает протокол, согласно которому такие объекты, как массивы, могут последовательно перебираться циклами и spread-оператором.

Читайте также: Как работает spread-оператор в JavaScript

Такой процесс перебора элементов внутри объекта называется итерацией, а объекты, поддерживающие эту функцию, называются итеративными объектами (также встречаются названия «итерируемые» и «перебираемые» объекты). К итеративным объектам в JavaScript относятся карты, массивы и наборы с итерируемым свойством. Простые объекты по умолчанию не являются итеративными.

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

Сам механизм итерации можно разделить на два компонента: это итерируемый объект (как вы уже знаете, это собственно структура данных) и итератор (это своего рода указатель, который перемещается по итерируемому объекту). Рассмотрим, например, массив: когда массив помещается в цикл for … of, вызывается итерируемое свойство, которое возвращает iterator. Это свойство относится к пространству имен Symbol.iterator, а объект, который оно возвращает, может использоваться в общем для всех структур управления циклом интерфейсе.

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

Когда итератор перемещается по структуре данных и последовательно извлекает элементы, возвращаемый объект содержит свойства value и done.

Свойство value содержит текущее значение, на которое указывает итератор, а done – это логическое значение, которое сообщает нам, достиг ли итератор последнего элемента в структуре данных.

Комбинация {value, done} используется такими структурами, как циклы. А как итератор вызывает следующий объект? Для этого используется метод next(), определенный в методе Symbol.iterator().

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

Объекты и итерация

Почему в обычных объектах JavaScript не принято использовать итерацию? Вот некоторые причины:

  • Одна из ключевых особенностей объекта – это то, что он определяется пользователем. Поэтому добавление [Symbol.iterator]() в объект будет неприятным сюрпризом.
  • Если вы хотите перебрать элементы верхнего уровня в объекте, можно использовать другой цикл, for…in.
  • Возможно, если вам нужна итерация, вместо обычных объектов можно использовать карты.

Все вышеперечисленные причины (пожалуй, кроме последней – некоторым пользователям слишком комфортно работать с обычными объектами, чтобы переходить на карты) являются достаточно вескими, чтобы не использовать итерацию в объектах.

На случай если это все же необходимо, вот простая реализация итеративного объекта:

let Reptiles = {
  biomes: {
    water: ["Alligators", "Crocs"],
    land: ["Snakes", "Turtles"]
  },

  [Symbol.iterator]() {
    let reptilesByBiome = Object.values(this.biomes);
    let reptileIndex = 0;
    let biomeIndex = 0;
    return {
      next() {
        if (reptileIndex >= reptilesByBiome[biomeIndex].length) {
          biomeIndex++;
          reptileIndex = 0;
        }

        if (biomeIndex >= reptilesByBiome.length) {
          return { value: undefined, done: true };
        }

        return {
          value: reptilesByBiome[biomeIndex][reptileIndex++],
          done: false
        };
      }
    };
  }
};

// let's now iterate over our new `Reptiles` iterable:
for (let reptile of Reptiles) console.log(reptile);

Это вернет такой вывод:

Alligators
Crocs
Snakes
Turtles

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

Извлечение итератора

Циклы типа for…of имеют встроенный механизм для выполнения итерации до тех пор, пока свойство done не получит значение true. Но что, если нам нужно использовать итерацию без встроенного цикла? Все просто: мы получаем итератор из итерируемого объекта, а затем вручную вызываем для него метод next().

Вернемся к нашему предыдущему примеру: в такой ситуации мы могли бы получить итератор из Reptiles, вызвав его Symbol.iterator следующим образом:

let reptileIterator = Reptiles[Symbol.iterator]();

Затем итератор можно использовать следующим образом:

console.log(reptileIterator.next());
// {value: "Alligators", done: false}
console.log(reptileIterator.next());
// {value: "Crocs", done: false}
console.log(reptileIterator.next());
// {value: "Snakes", done: false}
console.log(reptileIterator.next());
// {value: "Turtles", done: false}
console.log(reptileIterator.next());
// {value: undefined, done: true}

console.log(reptileIterator.next());
// TypeError: Cannot read property 'length' of undefined

Как видите, итератор содержит метод next(), который возвращает следующее значение в итерируемом объекте. Значение свойства done оценивается как истинное только тогда, когда было возвращено последнее значение и вызов next() после него. Поэтому для прохождения всего объекта вызовов next() всегда должно быть на один больше, чем элементов в объекте. Лишний вызов next() приведет к ошибке TypeError.

Заключение

Надеемся, из этого короткого материала вы смогли немного больше узнать о внутреннем устройстве JavaScript и таких структур данных, как объекты. Это лишь малая часть того, что умеют итеративные объекты. Если вы хотите узнать о них больше, предлагаем вам прочитать этот материал.

Tags:

Добавить комментарий