Генераторы в EcmaScript 6

В JavaScript все функции работают до завершения и пока одна функция не закончит свою работу, то другая операция не будет выполняться . По крайней мере так было до появления генераторов в ES6.

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

Объявление генератора

 function* foo(){}
 function * foo(){}
 function *foo(){}
 function*foo(){}

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

Пример работы генератора

Для большего понимания, как работает функция-генератор давайте приведем пример:

//объявление генератора
function *generate(){
   yield "Я первый";
   yield "Я второй";
   return "Ну и наконец я";
}

let gen = generate();

console.log(gen.next()); // {value: "Я первый", done: false;}
console.log(gen.next()); // {value: "Я второй", done: false;}
console.log(gen.next()); // {value: "Ну и наконец я", done: false;}
console.log(gen.next()); // {value: undefined, done: true;}

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

В отличии от return который отрабатывает единожды и тем самым завершает работу функции, yield может возвращать значение неопределенное количество раз.

При каждом вызове метода next() возвращается объект с ключами value (что по сути является возвращаемым значением итерации) и done (значение сигнализирующее программе о завершении последнего цикла итерации).

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

Важно заметить, что при последнем вызове метода next() значение свойства done будет равно true, тем самым итератор дает понять программе, что объект перебран и нужно завершать цикл итераций.

Так же запуск генератора привычными нам способом, например
generate(), не приводит к исполнению его кода. Вместо этого создается итератор, принцип работы его такой же как и у итерируемого объекта.

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

Как можно использовать функции-генераторы на практике?

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

Пример №1

Так как генератор основан на реализации итераторов, то с помощью него можно заменить многие методы, основывающиеся на итераторах, так как зачастую реализация генератора выглядит понятнее и короче. Давайте создадим метод range(), который принимает начальное и конечное значение, после чего выводит их диапазон.

Вот так бы мы это сделали с помощью итераторов

 function range(start, end){

   let current = start;

   return{
      [Symbol.iterator](){
         return{
            next(){  
               if(current <= end){
                  return {
                     value: current++, 
                     done: false
                  }
               }else{
                  return {
                     value: undefined, 
                     done: true
                  }
               }
            }
         }
      }
   }
}

let rangeObj = range(1,3);

for (const value of rangeObj) {
   console.log(value); //1,2,3
}

А так этот пример мы можем реализовать с использованием генератора

 function *range(start, stop) {
for (var i = start; i <= stop; i++)
   yield i;
}

let rangeObj = range(1,3);

for (const value of rangeObj) {
   console.log(value); //1,2,3
}

Как мы видим, описание метода range(), используя генераторы, выглядит намного красивее, понятнее и короче.

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

Пример №2

Так же можно привести пример реализации взаимодействия пользователя с DOM используя генераторы:

See the Pen Generator ES6 use case with DOM by Oleg (@oziside) on CodePen.

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

Делегирование yield в генераторах.

Выражению yield можно не только передать значение, которое он возвращает во внешний код, но и сам генератор или любой итерируемый объект, например:

function *foo(){
   yield 1;
   yield 2;
   yield 3;
}

//делегируем генератору bar() генератор foo()
function *bar(){
    yield *foo();
    yield 4;
}

let gener = bar();

for (const value of gener) {
   console.log(value); //1,2,3,4
}

Объект итератор, созданный с помощью функции-генератора *bar(), получает значения генератора *foo() , как раз таки путем делегирования при помощи ключевого слова yield* ( звездочка, как и при объявлении генератора обязательна и синтаксис записи такой же ). Тем самым для одного генератора можно создавать множество вспомогательных функций-генераторов.

Прерывание генератора и обработка ошибок.

Для более предсказуемой работы генератора и обработки исключений, существуют два метода .return() и .throw()

Метод return()

function * generator() {
  yield 1;
  yield 2;
  yield 3;
}

let gener = generator();

console.log(gener.next());// {value: 1, done: true}
console.log(gener.return("Остановка"));// {value: "Остановка", done: true}
console.log(gener.next());// {value: undefined, done: true} - все последующие вызовы вернут тоже самое

Вызов метода return() прерывает работу генератора и последующие вызовы метода next() будут возвращать объект со свойством done равному true, которое указывает на завершение работы генератора. Аргумент, передаваемый в метод return() не обязателен и нужен только для передачи произвольного значения.

Метод throw()

function * generator() {
  yield 1;
  yield 2;
  yield 3;
}

const gen = generator();

gen.throw('Something bad'); // Error Uncaught Something bad
gen.next(); // {value: undefined, done: true}

Обычная обработка ошибок, работающая в блоке исключений try..catch