DevGang
Авторизоваться

Итераторы и генераторы прекрасно работают вместе

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

Итератор

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

Условное обозначение

Symbol - это тип примитива, который гарантированно будет уникальным. Одной из его основных целей является использование в качестве ключа для словарных коллекций (карт или простых объектов). Кроме того, тот факт, что он всегда уникален, делает его хорошим кандидатом для других интересных вариантов использования. Теперь давайте рассмотрим, что я имею в виду, когда говорю что символ действительно уникален:

const symbol = Symbol('Unique');
const notSymbol = 'Unique';

if (notSymbol !== symbol) {
   console.log(`String ${notSymbol} is not equal to ${symbol.toString()}`);
}

const symbol2 = Symbol('Unique');

if (symbol !== symbol2) {
   console.log(`${symbol.toString()} is not equal to ${symbol2.toString()}`);
}

Код выше напечатает:

// String Unique is not equal to Symbol(Unique)
// Symbol(Unique) is not equal to Symbol(Unique)

Сначала с помощью if мы сравниваем переменную notSymbol со значением Unique с экземпляром символа с аналогичным значением описания Unique. Как видите, эти две переменные не равны. Более интересный случай - второй if, где мы сравниваем два символа, которые на первый взгляд могут показаться идентичными. Однако даже если оба символа имеют одинаковое значение описания, они не равны. Описание используется исключительно для улучшения читаемости нашего кода.

Определение итераторируемого объекта и итератора

JavaScript предоставляет статический символ Symbol.iterator, который мы можем использовать, чтобы сделать любой объект итеративным. Давайте посмотрим на этот пример:

Сначала давайте определим класс, который мы будем повторять:

class PetShelter {
   constructor() {
       this.pets = [
           { type: 'cat', name: 'Emma' },
           { type: 'dog', name: 'Bubbles' },
           { type: 'dog', name: 'Hank' },
           { type: 'cat', name: 'Leo' },
       ]
       this[Symbol.iterator] = function() {
           return new PetIterator(this);
       }
   }
}

PetShelter простой класс, который содержит массив объектов с именем pets. Ниже нашего мы добавляем другое свойство, используя Symbol.iterator в качестве ключа. Мы назначаем ему функцию, которая возвращает новый экземпляр PetIterator.

Теперь посмотрим, как выглядит PetIterator:

class PetIterator {
   constructor(shelter) {
       this.shelter = shelter;
       this.counter = 0;
   }

   next() {
       if (this.counter >= this.shelter.pets.length) 
           return { done: true };

       const result = {
           done: false,
           value: this.shelter.pets[this.counter]
       };

       this.counter++;

       return result;
   }
}

PetIterator это просто еще один класс, который содержит только один метод - next. Каждый итератор должен иметь метод next. Кроме того, этот метод должен возвращать объект, который может содержать два свойства: done и value. Вы уже могли догадаться, для чего используются эти свойства:

  • done является логическим свойством, которое указывает, достигли мы конца итерации или нет.
  • value используется для возврата значения текущего итерированного элемента.

Используя их вместе

Теперь, когда у нас есть итерируемый объект и итератор, давайте посмотрим, как мы можем использовать их вместе:

const shelter = new PetShelter();

for (let pet of shelter) {
   console.log({ pet });
}

Довольно просто, верно? Цикл for...of автоматически создает и возвращает новый экземпляр итератора, предоставленного классом PetShelter.

Приведенный выше код выведет в консоль следующее:

// { pet: { type: 'cat', name: 'Emma' } }
// { pet: { type: 'dog', name: 'Bubbles' } }
// { pet: { type: 'dog', name: 'Hank' } }
// { pet: { type: 'cat', name: 'Leo' } }

Генератор

Генераторы - это одна из новых синтаксических функций, представленных в спецификации EcmaScript 2015. Точно так же как и async, генераторы могут использоваться для управления потоком выполнения программы, приостанавливая и возобновляя его, как клиент считает нужным. Давайте посмотрим на пример того, как мы будем определять генератор:

function * generatorSample() {
   yield 'Hi'
   yield 'What\'s your name?'
   console.log('I\'m done here');
}

Как видите, генераторы - это просто обычные функции с несколькими незначительными отличиями:

  • Чтобы определить генератор, вы должны поставить * после ключевого слова function.
  • yield может использоваться внутри генератора, чтобы приостановить выполнение. Кроме того, любое значение после ключевого слова yield передается обратно клиенту, который вызывает генератор.

Теперь давайте посмотрим, как мы можем использовать наш генератор:

const genr = generatorSample() // returns a new generator instance

console.log(genr.next().value);
console.log(genr.next().value);
genr.next();

Вот вывод кода выше:

// Hi
// What's your name?
// I'm done here

Несколько вещей, чтобы принять к сведению:

  • Вызов generatorSample() не запускает функцию генератора, а вместо этого возвращает новый экземпляр генератора.
  • Как только код внутри нашего генератора встречается yield, он останавливается. И мы используем метод генератора next, чтобы продолжить выполнение.

Вот как выглядит структура возвращаемого объекта:

{
value: 'Hi' //yielded value,
done: false //whether the generator is done executing or not
}

Использование генераторов для создания итераторов

Выглядит знакомо? Надеюсь, теперь вы начинаете видеть связь между итераторами и генераторами. Честно говоря, одна из главных причин, по которой генераторы были представлены в ES 2015, заключалась в том, чтобы значительно упростить процесс создания итераторов. Фактически, все, что нам нужно сделать, чтобы превратить нашу функцию PetShelter в итерируемую, это сделать, чтобы Symbol.iterator функция make[ ] возвращала новый экземпляр генератора. Теперь давайте посмотрим, как мы можем сделать это:

const PetIterator = function * (shelter) {
   for (let i = 0; i < shelter.pets.length; i++) {
       yield shelter.pets[i];
   }
}

class PetShelter {
   constructor() {
       this.pets = [
           { type: 'cat', name: 'Emma' },
           { type: 'dog', name: 'Bubbles' },
           { type: 'dog', name: 'Hank' },
           { type: 'cat', name: 'Leo' },
       ]
       this[Symbol.iterator] = function () {
           return PetIterator(this);
       }
   }
}

const shelter = new PetShelter();

for (let pet of shelter) {
   console.log({ pet });
}

Как и в предыдущем примере, приведенный выше код выдаст следующее:

// { pet: { type: 'cat', name: 'Emma' } }
// { pet: { type: 'dog', name: 'Bubbles' } }
// { pet: { type: 'dog', name: 'Hank' } }
// { pet: { type: 'cat', name: 'Leo' } }

Как видите, нам больше не нужно определять отдельный класс для нашего PetIterator. Все, что нам нужно было определить, это генератор, который использует цикл for для перебора свойства pets. Затем мы настроили функцию [Symbol.iterator] так, чтобы она возвращала новый экземпляр генератора PetIterator.

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

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

Спасибо за чтение!

#JavaScript
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

Поделитесь своим опытом, расскажите о новом инструменте, библиотеке или фреймворке. Для этого не обязательно становится постоянным автором.

Попробовать

Сделайте первый шаг к новой профессии

Получить скидку