Владислав Калачев

Учимся кодить на JavaScript. Генераторы на примерах.

11

Доброго времени суток, друзья. Коллекция — структура данных, с которой любой разработчик на практике встречается очень часто. Для удобной работы с данными JavaScript содержит набор стандартных коллекций: Object, Array, Set, Map, WeakMap, WeakSet, все  они имеют свои методы. Итераторы и генераторы дают еще один способ для работы с коллекциями. Давайте рассмотрим каждый из этих терминов более подробно и поработаем с ними на практике.

Что такое Итератор?

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

Пример:


function iterator(array){
    let id = 0;
    return {
       next: function(){
           return id < array.length ?
               {value: array[id ++], done: false} :
               {done: true};
       }
    }
}

const text = iterator(['Hello', 'World']);
console.log(text.next()); // { value: 'Hello', done: false }
console.log(text.next()); // { value: 'World', done: false } 
console.log(text.next()); // { done: true }

В примере выше сперва мы инициализируем функцию iterator, в параметры которой передаем массив со значениями Hello, World. При первых двух вызовах метода next() получим объект со значением самих слов и свойством done: false, которое говорит нам, что итератор еще не закончился. Вызвав метод повторно получаем объект со значением done: true и все последующие вызовы мы также будем получать этот объект, тем самым итератор информирует нас, что перебирать в коллекции больше нечего.

Думаю, что сейчас вы уже получили представление об итераторах, перейдем теперь к генераторам.

Что такое Генератор?

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

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

Пример:


function hello() {
    console.log('Hello');
    console.log('World');
}

Данная функция при ее вызове вернет нам сразу два вызова «консоли», но по условию нам требуется вернуть из коллекции «порционно» определенное значение при условии, что мы знаем на каком шаге какое значение существует. Для решения именно таких задач прекрасно подойдет функция-генератор.

Перепишем предыдущий пример:


function* generate() {
    yield 'Hello';
    yield 'World';
}

let text = generate();
console.log(text.next()); // { value: 'Hello', done: false }
console.log(text.next()); // { value: 'Hello', done: false }
console.log(text.next()); // { value: undefined, done: true }

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

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

Метод return

Функцию-генератор можно прекратить в любой момент. Для этого в объекте «Генератор» есть специальный метод return.

Пример:


function* generate() {
    yield 'Hello';
    yield 'World';
}
let text = generate();
console.log(text.return()); // { value: undefined, done: true }
console.log(text.next()); // { value: undefined, done: true }

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

Пример:


function* generate() {
    yield 'Hello';
    yield 'World';
}
let text = generate();
console.log(text.return('Hello')); // { value: «Hello», done: true }

Метод throw

Для работы с исключениями в объекте «Генератор» присутствует специальный метод throw() в параметр которого можно передать само исключение и уже затем обработать его в самом генераторе. Для лучшего понимания посмотрим на пример:


function* generate() {
  while(true) {
    try {
       yield 'Hello';
    } catch(e) {
      console.log('Error!');
    }
  }
}

let gen = generate();
gen.next();
// { value: 'Hello', done: false }
g.throw(new Error('Error'));
// "Error caught!"

Используем for..of 

Для перебора генератора и получения всех значений коллекции можно воспользоваться циклом for..of.

Пример:


function* generate() {
  yield 'Hello';
  return 'World';
}

let generator = generate();

for(let value of generator) {
  console.log(value);
}

В данном примере мы возвращаем последнее значение, используя return. Данная запись закончит выполнение генератора со значением done: true на последнем шаге, это означает, что результатом выполнения цикла for..of будет только значение «Hello». Для того, чтобы получить последнее значение, требуется переписать return на yield.

Пример:


function* generate() {
  yield 'Hello';
  yield 'World';
}

let generator = generate();

for(let value of generator) {
  console.log(value);
}

Теперь будут получены все необходимые значения.

Композиция генераторов

Благодаря своей гибкости генераторы могут возвращать генераторы. Это позволяет создавать композиции из генераторов. Для выполнения такой задачи достаточно при написании оператора yield добавить символ * после чего указать название другого генератора.

Пример:


function* generate1() {
  yield 1;
  yield* generate2();
 }

function* generate2() {
  yield 2;
  yield 3;
  yield 4;
}

let gen = generate1();
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: 3, done: false }
console.log(gen.next()); // { value: 4, done: false }
console.log(gen.next()); // { value: undefined, done: true }

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

Вы можете спросить у меня зачем вообще нужно знать про генераторы? На практике не зная генераторы разобраться в работе под «капотом» async/await не получится, да и полноценно работать с Redux-Saga тоже.

Заключение

Если коротко, то в этой статье мы рассмотрели следующие темы:‐Итератор - это объект, который имеет метод next() для перебора коллекции;

‐ Генератор - это функция, которая является фабрикой итераторов;

‐ Работа с методом next;

‐ Работа с методом return;

‐ Работа с методом throw;

‐ Для перебора генератора используется цикл for..of;

‐ Композиция генераторов. Использование оператора yield*

Надеюсь, что данный материал был вам полезен. Учитесь, думайте, пишите код. Удачного кодинга, друзья!

Дополнительные материалы:

https://developer.mozilla.org/ru/docs/Web/JavaScript/Guide/Iterators_and_Generators - документация от Mozilla

Подписывайтесь на наш канал в Telegram для получения самой последней и актуальной информации. 

0

Комментарии:

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

Ваш адрес email не будет опубликован. Обязательные поля помечены *