Итераторы и генераторы

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

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

Генераторы близки к итераторам. Генератор это функция, которой не обязательно выполняться до конца когда мы запускаем её. Это функция, которая yields разные значения.

Самая большая польза когда вы исползуете их вместе.

Давайте посмотрим на практике. Создадим просто массив.
У массивов также содержатся well-known символы. Для итерации воспользуемся символом [Symbol.iterator]. По сути это функция которая используется для итерирование по итерируемым объектам. Если мы вызовем эту функцию и передадим результат в переменную, то получим объект, которому наследуется метод next() и свойство done, которое имеет значение false до тех пор пока не достигнем следующего за последним элементом.

let array = [1, 2, 3];
console.log(typeof array[Symbol.iterator]); //function

const it = array[Symbol.iterator]();
console.log(it); //Array Iterator {}
console.log(it.next()); //value: 1

Вы можете сделать любой объект итерируемым. Всё что Вам нужно сделать это указать [Symbol.iterator] символ там. И у вас появится возможность пройтись по значениям вашего объекта. Это функция является ядром каждого итератора. Мы также можем переписать поведение итератора. Итератор это функция, и она возвратит объект, и мы знаем что этот объект наследует метод next, который мы можем перезаписать.

array[Symbol.iterator] = () => {
  let nextValue = 10;
  return {
    next: () => {
      nextValue++;
      return {
        done: nextValue > 15 ? true : false,
        value: nextValue
      };
    }
  };
};

it = array[Symbol.iterator]();

for (let el of els) {
  console.log(el);
}

Давай создадим свой итератор.
Создаем объект, в нем указываем объект итератора. JS когда видит его понимает что объект с итератором и итерируемый.

let person = {
  name: "Eugene",
  hobbies: ["Sports", "Cooking", "Riding"],
  [Symbol.iterator]() {
    let i = 0;
    let j = 0;
    let hobbies = this.hobbies;
    return {
      next() {
        i++;
        i %= hobbies.length;
        j = i ? j : ++j;
        let value = hobbies[i];
        return {
          done: j > 3,
          value: value
        };
      }
    };
  }
};

for (let hobby of person) {
  console.log(hobby);
}

Генераторы

Генераторы близки к концепту итераторов.
Генераторы выглядят как обычная функция только нужно указать астериск перед названием функции (в любом месте между ключевым словом function и названием функции).

function* select() {}

Для того чтобы генератор заработал, мы должны использовать ключевое слово yield.
Это ключевое слово играет роль ключевого слова return

function* select() {
  yield "House";
  yield "Garage";
}

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

function* select() {
  yield "House";
  yield "Garage";
}

const it = select();
console.log(it.next()); //{value: "House", done: false}
console.log(it.next()); //{value: "Garage", done: false}
console.log(it.next()); //{value: undefined, done: true}

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

let obj = {
  [Symbol.iterator]: gen
};

function* gen() {
  yield 1;
  yield 2;
}

for (let element of obj) {
  console.log(element);
}

// Или
function* gen(end) {
  for (let i = 0; i < end; i++) {
    yield i;
  }
}

let it = gen(10);

console.log(it.next());

Управление генератором через throw и return

Мы можем использовать метод throw чтобы выкинуть ошибку.
Также у нас есть метод return, но на сегодняшний день он имеет поддержку только в Firefox.

function* gen(end) {
  for (let i = 0; i < end; i++) {
    try {
      yield i;
    } catch (e) {
      throw new Error(e);
    }
  }
}

let it = gen(10);

console.log(it.next());
console.log(it.next());
console.log(it.throw("Some error"));
console.log(it.next());
console.log(it.next());
console.log(it.return('Hello')); //{done: false, value: 'Hello'}