Для чего нужны генераторы js
Перейти к содержимому

Для чего нужны генераторы js

  • автор:

Генераторы

Обычные функции возвращают только одно-единственное значение (или ничего).

Генераторы могут порождать (yield) множество значений одно за другим, по мере необходимости. Генераторы отлично работают с перебираемыми объектами и позволяют легко создавать потоки данных.

Функция-генератор

Для объявления генератора используется специальная синтаксическая конструкция: function* , которая называется «функция-генератор».

Выглядит она так:

function* generateSequence()

Функции-генераторы ведут себя не так, как обычные. Когда такая функция вызвана, она не выполняет свой код. Вместо этого она возвращает специальный объект, так называемый «генератор», для управления её выполнением.

function* generateSequence() < yield 1; yield 2; return 3; >// "функция-генератор" создаёт объект "генератор" let generator = generateSequence(); alert(generator); // [object Generator]

Выполнение кода функции ещё не началось:

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

Результатом метода next() всегда является объект с двумя свойствами:

  • value : значение из yield .
  • done : true , если выполнение функции завершено, иначе false .

Например, здесь мы создаём генератор и получаем первое из возвращаемых им значений:

function* generateSequence() < yield 1; yield 2; return 3; >let generator = generateSequence(); let one = generator.next(); alert(JSON.stringify(one)); //

На данный момент мы получили только первое значение, выполнение функции остановлено на второй строке:

Повторный вызов generator.next() возобновит выполнение кода и вернёт результат следующего yield :

let two = generator.next(); alert(JSON.stringify(two)); //

И, наконец, последний вызов завершит выполнение функции и вернёт результат return :

let three = generator.next(); alert(JSON.stringify(three)); //

Сейчас генератор полностью выполнен. Мы можем увидеть это по свойству done:true и обработать value:3 как окончательный результат.

Новые вызовы generator.next() больше не имеют смысла. Впрочем, если они и будут, то не вызовут ошибки, но будут возвращать один и тот же объект: .

function* f(…) или function *f(…) ?

Нет разницы, оба синтаксиса корректны.

Но обычно предпочтителен первый вариант, так как звёздочка относится к типу объявляемой сущности ( function* – «функция-генератор»), а не к её названию, так что резонно расположить её у слова function .

Перебор генераторов

Как вы, наверное, уже догадались по наличию метода next() , генераторы являются перебираемыми объектами.

Возвращаемые ими значения можно перебирать через for..of :

function* generateSequence() < yield 1; yield 2; return 3; >let generator = generateSequence(); for(let value of generator) < alert(value); // 1, затем 2 >

Выглядит гораздо красивее, чем использование .next().value , верно?

…Но обратите внимание: пример выше выводит значение 1 , затем 2 . Значение 3 выведено не будет!

Это из-за того, что перебор через for..of игнорирует последнее значение, при котором done: true . Поэтому, если мы хотим, чтобы были все значения при переборе через for..of , то надо возвращать их через yield :

function* generateSequence() < yield 1; yield 2; yield 3; >let generator = generateSequence(); for(let value of generator) < alert(value); // 1, затем 2, затем 3 >

Так как генераторы являются перебираемыми объектами, мы можем использовать всю связанную с ними функциональность, например оператор расширения . :

function* generateSequence() < yield 1; yield 2; yield 3; >let sequence = [0, . generateSequence()]; alert(sequence); // 0, 1, 2, 3

В коде выше . generateSequence() превращает перебираемый объект-генератор в массив элементов (подробнее ознакомиться с оператором расширения можно в главе Остаточные параметры и оператор расширения)

Использование генераторов для перебираемых объектов

Некоторое время назад, в главе Перебираемые объекты, мы создали перебираемый объект range , который возвращает значения from..to .

Давайте вспомним код:

let range = < from: 1, to: 5, // for..of range вызывает этот метод один раз в самом начале [Symbol.iterator]() < // . он возвращает перебираемый объект: // далее for..of работает только с этим объектом, запрашивая следующие значения return < current: this.from, last: this.to, // next() вызывается при каждой итерации цикла for..of next() < // нужно вернуть значение как объект if (this.current ; > else < return < done: true >; > > >; > >; // при переборе объекта range будут выведены числа от range.from до range.to alert([. range]); // 1,2,3,4,5

Мы можем использовать функцию-генератор для итерации, указав её в Symbol.iterator .

Вот тот же range , но с гораздо более компактным итератором:

let range = < from: 1, to: 5, *[Symbol.iterator]() < // краткая запись для [Symbol.iterator]: function*() for(let value = this.from; value > >; alert( [. range] ); // 1,2,3,4,5

Это работает, потому что range[Symbol.iterator]() теперь возвращает генератор, и его методы – в точности то, что ожидает for..of :

  • у него есть метод .next()
  • который возвращает значения в виде

Это не совпадение, конечно. Генераторы были добавлены в язык JavaScript, в частности, с целью упростить создание перебираемых объектов.

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

Генераторы могут генерировать бесконечно

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

Конечно, нам потребуется break (или return ) в цикле for..of по такому генератору, иначе цикл будет продолжаться бесконечно, и скрипт «зависнет».

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

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

Например, у нас есть функция для генерации последовательности чисел:

function* generateSequence(start, end)

Мы хотели бы использовать её при генерации более сложной последовательности:

  • сначала цифры 0..9 (с кодами символов 48…57)
  • за которыми следуют буквы в верхнем регистре A..Z (коды символов 65…90)
  • за которыми следуют буквы алфавита a..z (коды символов 97…122)

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

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

Для генераторов есть особый синтаксис yield* , который позволяет «вкладывать» генераторы один в другой (осуществлять их композицию).

Вот генератор с композицией:

function* generateSequence(start, end) < for (let i = start; i function* generatePasswordCodes() < // 0..9 yield* generateSequence(48, 57); // A..Z yield* generateSequence(65, 90); // a..z yield* generateSequence(97, 122); >let str = ''; for(let code of generatePasswordCodes()) < str += String.fromCharCode(code); >alert(str); // 0..9A..Za..z

Директива yield* делегирует выполнение другому генератору. Этот термин означает, что yield* gen перебирает генератор gen и прозрачно направляет его вывод наружу. Как если бы значения были сгенерированы внешним генератором.

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

function* generateSequence(start, end) < for (let i = start; i function* generateAlphaNum() < // yield* generateSequence(48, 57); for (let i = 48; i let str = ''; for(let code of generateAlphaNum()) < str += String.fromCharCode(code); >alert(str); // 0..9a..zA..Z

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

yield – дорога в обе стороны

До этого момента генераторы сильно напоминали перебираемые объекты, со специальным синтаксисом для генерации значений. Но на самом деле они намного мощнее и гибче.

Всё дело в том, что yield – дорога в обе стороны: он не только возвращает результат наружу, но и может передавать значение извне в генератор.

Чтобы это сделать, нам нужно вызвать generator.next(arg) с аргументом. Этот аргумент становится результатом yield .

Продемонстрируем это на примере:

function* gen() < // Передаём вопрос во внешний код и ожидаем ответа let result = yield "2 + 2 = ?"; // (*) alert(result); >let generator = gen(); let question = generator.next().value; // передаём результат в генератор
  1. Первый вызов generator.next() – всегда без аргумента, он начинает выполнение и возвращает результат первого yield «2+2=?» . На этой точке генератор приостанавливает выполнение.
  2. Затем, как показано на картинке выше, результат yield переходит во внешний код в переменную question .
  3. При generator.next(4) выполнение генератора возобновляется, а 4 выходит из присваивания как результат: let result = 4 .

Обратите внимание, что внешний код не обязан немедленно вызывать next(4) . Ему может потребоваться время. Это не проблема, генератор подождёт.

// возобновить генератор через некоторое время setTimeout(() => generator.next(4), 1000);

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

Чтобы сделать происходящее более очевидным, вот ещё один пример с большим количеством вызовов:

function* gen() < let ask1 = yield "2 + 2 = ?"; alert(ask1); // 4 let ask2 = yield "3 * 3 = ?" alert(ask2); // 9 >let generator = gen(); alert( generator.next().value ); // "2 + 2 = ?" alert( generator.next(4).value ); // "3 * 3 = ?" alert( generator.next(9).done ); // true
  1. Первый .next() начинает выполнение… Оно доходит до первого yield .
  2. Результат возвращается во внешний код.
  3. Второй .next(4) передаёт 4 обратно в генератор как результат первого yield и возобновляет выполнение.
  4. …Оно доходит до второго yield , который станет результатом .next(4) .
  5. Третий next(9) передаёт 9 в генератор как результат второго yield и возобновляет выполнение, которое завершается окончанием функции, так что done: true .

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

generator.throw

Как мы видели в примерах выше, внешний код может передавать значение в генератор как результат yield .

…Но можно передать не только результат, но и инициировать ошибку. Это естественно, так как ошибка является своего рода результатом.

Для того, чтобы передать ошибку в yield , нам нужно вызвать generator.throw(err) . В таком случае исключение err возникнет на строке с yield .

Например, здесь yield «2 + 2 = ?» приведёт к ошибке:

function* gen() < try < let result = yield "2 + 2 = ?"; // (1) alert("Выполнение программы не дойдёт до этой строки, потому что выше возникнет исключение"); >catch(e) < alert(e); // покажет ошибку >> let generator = gen(); let question = generator.next().value; generator.throw(new Error("Ответ не найден в моей базе данных")); // (2)

Ошибка, которая проброшена в генератор на строке (2) , приводит к исключению на строке (1) с yield . В примере выше try..catch перехватывает её и отображает.

Если мы не хотим перехватывать её, то она, как и любое обычное исключение, «вывалится» из генератора во внешний код.

Текущая строка вызывающего кода – это строка с generator.throw , отмечена (2) . Таким образом, мы можем отловить её во внешнем коде, как здесь:

function* generate() < let result = yield "2 + 2 = ?"; // Ошибка в этой строке >let generator = generate(); let question = generator.next().value; try < generator.throw(new Error("Ответ не найден в моей базе данных")); >catch(e) < alert(e); // покажет ошибку >

Если же ошибка и там не перехвачена, то дальше – как обычно, она выпадает наружу и, если не перехвачена, «повалит» скрипт.

Итого

  • Генераторы создаются при помощи функций-генераторов function* f(…) .
  • Внутри генераторов и только внутри них существует оператор yield .
  • Внешний код и генератор обмениваются промежуточными результатами посредством вызовов next/yield .

В современном JavaScript генераторы используются редко. Но иногда они оказываются полезными, потому что способность функции обмениваться данными с вызывающим кодом во время выполнения совершенно уникальна. И, конечно, для создания перебираемых объектов.

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

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

Задачи

Псевдослучайный генератор

Есть много областей, где нам нужны случайные данные.

Одной из них является тестирование. Нам могут понадобиться случайные данные: текст, числа и т.д., чтобы хорошо всё проверить.

В JavaScript мы можем использовать Math.random() . Но если что-то пойдёт не так, то нам нужно будет перезапустить тест, используя те же самые данные.

Для этого используются так называемые «сеяные псевдослучайные генераторы». Они получают «зерно», как первое значение, и затем генерируют следующее, используя формулу. Так что одно и то же зерно даёт одинаковую последовательность, и, следовательно, весь поток легко воспроизводим. Нам нужно только запомнить зерно, чтобы воспроизвести последовательность.

Пример такой формулы, которая генерирует более-менее равномерно распределённые значения:

next = previous * 16807 % 2147483647

Если мы используем 1 как зерно, то значения будут:

  1. 16807
  2. 282475249
  3. 1622650073
  4. …и так далее…

Задачей является создать функцию-генератор pseudoRandom(seed) , которая получает seed и создаёт генератор с указанной формулой.

let generator = pseudoRandom(1); alert(generator.next().value); // 16807 alert(generator.next().value); // 282475249 alert(generator.next().value); // 1622650073

О генераторах в JavaScript ES6, и о том, почему изучать их необязательно

С ростом популярности конструкции async/await растёт и интерес к её внутренним механизмам. Порывшись в интернете, несложно выяснить, что в основе async/await лежат широко известные промисы, и генераторы, которые пользуются куда меньшей известностью и популярностью.

Материал, перевод которого мы сегодня публикуем, посвящён генераторам. А именно, тут мы поговорим о том, как они работают, и о том, как они, совместно с промисами, используются в недрах конструкции async/await. Автор этой статьи говорит, что генераторы, ради их практического применения, осваивать необязательно. Кроме того, он отмечает, что он рассчитывает на то, что читатель немного разбирается в промисах.

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

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

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

function makeIterator(array) < var nextIndex = 0; console.log("nextIndex =>", nextIndex); return < next: function() < return nextIndex < array.length ? < value: array[nextIndex++], done: false >: < done: true >; > >; > var it = makeIterator(["simple", "iterator"]); console.log(it.next()); // console.log(it.next()); // console.log(it.next()); //

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

Теперь поговорим о генераторах. Генераторы — это функции, которые работают как фабрики итераторов. Рассмотрим простой пример, а затем поговорим о двух механизмах, имеющих отношение к генераторам.

function* sample() < yield "simple"; yield "generator"; >var it = sample(); console.log(it.next()); // console.log(it.next()); // console.log(it.next()); //

Обратите внимание на звёздочку в объявлении функции. Это указывает на то, что данная функция является генератором. Кроме того, взгляните на ключевое слово yield . Оно приостанавливает выполнение функции и возвращает некое значение. Собственно, эти две особенности и являются теми самыми двумя механизмами, о которых мы говорили выше:

  • Функция-генератор — это функция, объявленная с использованием звёздочки около ключевого слова function или около имени функции.
  • Итератор генератора создаётся, когда вызывают функцию-генератор.

Теперь, когда мы разобрались в основах, поговорим о более интересных вещах. Итераторы и генераторы могут обмениваться данными в двух направлениях. А именно, генераторы, с помощью ключевого слова yield , могут возвращать значения итераторам, однако и итераторы могут отправлять данные генераторам, используя метод iterator.next(‘someValue’) . Вот как это выглядит.

function* favBeer() < const reply = yield "What is your favorite type of beer?"; console.log(reply); if (reply !== "ipa") return "No soup for you!"; return "OK, soup."; > < const it = favBeer(); const q = it.next().value; // Итератор задаёт вопрос console.log(q); const a = it.next("lager").value; // Получен ответ на вопрос console.log(a); >// What is your favorite beer? // lager // No soup for you! < const it = favBeer(); const q = it.next().value; // Итератор задаёт вопрос console.log(q); const a = it.next("ipa").value; // получен ответ на вопрос console.log(a); >// What is your favorite been? // ipa // OK, soup.

Генераторы и промисы

Теперь мы можем поговорить о том, как генераторы и промисы формируют базу конструкции async/await. Представьте, что вместо того, чтобы возвращать, с помощью ключевого слова yield , некие значения, генератор возвращает промисы. При таком раскладе генератор можно обернуть в функцию, которая будет ожидать разрешения промиса и возвращать значение промиса генератору в методе .next() , как было показано в предыдущем примере. Существует популярная библиотека, co, которая выполняет именно такие действия. Выглядит это так:

co(function* doStuff()< var result - yield someAsyncMethod(); var another = yield anotherAsyncFunction(); >);

Итоги

По мнению автора этого материала JS-разработчикам нужно знать о том, как работают генераторы, лишь для того, чтобы понимать особенности внутреннего устройства конструкции async/await. А вот использовать их непосредственно в собственном коде не стоит. Генераторы вводят в JavaScript возможность приостанавливать выполнение функции и возвращаться к ней когда (и если) разработчик сочтёт это необходимым. До сих пор мы, работая с JS-функциями, ожидали, что они, будучи вызванными, просто выполняются от начала до конца. Возможность их приостанавливать — это уже что-то новое, но этот функционал удобно реализован в конструкции async/await.

С этим мнением, конечно, можно и поспорить. Например, один из аргументов в пользу генераторов, сводится к тому, что знание того, как они работают, полезно для отладки кода с async/await, так как внутри этой конструкции скрываются генераторы. Однако автор материала полагает, что это, всё же, нечто иное, нежели использование генераторов в собственном коде.

Уважаемые читатели! Что вы думаете о генераторах? Может быть, вы знаете какие-то варианты их использования, которые оправдывают их непосредственное применение в коде JS-проектов?

Царский промо-код для скидки в 10% на наши виртуальные сервера:

Генераторы

Материал на этой странице устарел, поэтому скрыт из оглавления сайта.

Более новая информация по этой теме находится на странице https://learn.javascript.ru/generators.

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

Создание генератора

Для объявления генератора используется новая синтаксическая конструкция: function* (функция со звёздочкой).

Её называют «функция-генератор» (generator function).

Выглядит это так:

function* generateSequence()

При запуске generateSequence() код такой функции не выполняется. Вместо этого она возвращает специальный объект, который как раз и называют «генератором».

// generator function создаёт generator let generator = generateSequence();

Правильнее всего будет воспринимать генератор как «замороженный вызов функции»:

При создании генератора код находится в начале своего выполнения.

Основным методом генератора является next() . При вызове он возобновляет выполнение кода до ближайшего ключевого слова yield . По достижении yield выполнение приостанавливается, а значение – возвращается во внешний код:

function* generateSequence() < yield 1; yield 2; return 3; >let generator = generateSequence(); let one = generator.next(); alert(JSON.stringify(one)); //

Повторный вызов generator.next() возобновит выполнение и вернёт результат следующего yield :

let two = generator.next(); alert(JSON.stringify(two)); //

И, наконец, последний вызов завершит выполнение функции и вернёт результат return :

let three = generator.next(); alert(JSON.stringify(three)); //

Функция завершена. Внешний код должен увидеть это из свойства done:true и обработать value:3 , как окончательный результат.

Новые вызовы generator.next() больше не имеют смысла. Впрочем, если они и будут, то не вызовут ошибки, но будут возвращать один и тот же объект: .

«Открутить назад» завершившийся генератор нельзя, но можно создать новый ещё одним вызовом generateSequence() и выполнить его.

function* (…) или function *(…) ?

Можно ставить звёздочку как сразу после function , так и позже, перед названием. В интернете можно найти обе эти формы записи, они верны:

function* f() < // звёздочка после function >function *f() < // звёздочка перед названием >

Технически, нет разницы, но писать то так то эдак – довольно странно, надо остановиться на чём-то одном.

Автор этого текста полагает, что правильнее использовать первый вариант function* , так как звёздочка относится к типу объявляемой сущности ( function* – «функция-генератор»), а не к её названию. Конечно, это всего лишь рекомендация-мнение, не обязательное к выполнению, работать будет в любом случае.

Генератор – итератор

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

Его можно перебирать и через for..of :

function* generateSequence() < yield 1; yield 2; return 3; >let generator = generateSequence(); for(let value of generator) < alert(value); // 1, затем 2 >

Заметим, однако, существенную особенность такого перебора!

При запуске примера выше будет выведено значение 1 , затем 2 . Значение 3 выведено не будет. Это потому что стандартный перебор итератора игнорирует value на последнем значении, при done: true . Так что результат return в цикле for..of не выводится.

Соответственно, если мы хотим, чтобы все значения возвращались при переборе через for..of , то надо возвращать их через yield :

function* generateSequence() < yield 1; yield 2; yield 3; >let generator = generateSequence(); for(let value of generator) < alert(value); // 1, затем 2, затем 3 >

…А зачем вообще return при таком раскладе, если его результат игнорируется? Он тоже нужен, но в других ситуациях. Перебор через for..of – в некотором смысле «исключение». Как мы увидим дальше, в других контекстах return очень даже востребован.

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

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

Разберём композицию на примере.

Пусть у нас есть функция generateSequence , которая генерирует последовательность чисел:

function* generateSequence(start, end) < for (let i = start; i > // Используем оператор … для преобразования итерируемого объекта в массив let sequence = [. generateSequence(2,5)]; alert(sequence); // 2, 3, 4, 5

Мы хотим на её основе сделать другую функцию generateAlphaNum() , которая будет генерировать коды для буквенно-цифровых символов латинского алфавита:

  • 48..57 – для 0..9
  • 65..90 – для A..Z
  • 97..122 – для a..z

Далее этот набор кодов можно превратить в строку и использовать, к примеру, для выбора из него случайного пароля. Только символы пунктуации ещё хорошо бы добавить для надёжности, но в этом примере мы будем без них.

Естественно, раз в нашем распоряжении есть готовый генератор generateSequence , то хорошо бы его использовать.

Конечно, можно внутри generateAlphaNum запустить несколько раз generateSequence , объединить результаты и вернуть. Так мы бы сделали с обычными функциями. Но композиция – это кое-что получше.

Она выглядит так:

function* generateSequence(start, end) < for (let i = start; i function* generateAlphaNum() < // 0..9 yield* generateSequence(48, 57); // A..Z yield* generateSequence(65, 90); // a..z yield* generateSequence(97, 122); >let str = ''; for(let code of generateAlphaNum()) < str += String.fromCharCode(code); >alert(str); // 0..9A..Za..z

Здесь использована специальная форма yield* . Она применима только к другому генератору и делегирует ему выполнение.

То есть, при yield* интерпретатор переходит внутрь генератора-аргумента, к примеру, generateSequence(48, 57) , выполняет его, и все yield , которые он делает, выходят из внешнего генератора.

Получается – как будто мы вставили код внутреннего генератора во внешний напрямую, вот так:

function* generateSequence(start, end) < for (let i = start; i function* generateAlphaNum() < // yield* generateSequence(48, 57); for (let i = 48; i let str = ''; for(let code of generateAlphaNum()) < str += String.fromCharCode(code); >alert(str); // 0..9A..Za..z

Код выше по поведению полностью идентичен варианту с yield* . При этом, конечно, переменные вложенного генератора не попадают во внешний, «делегирование» только выводит результаты yield во внешний поток.

Композиция – это естественное встраивание одного генератора в поток другого. При композиции значения из вложенного генератора выдаются «по мере готовности». Поэтому она будет работать даже если поток данных из вложенного генератора оказался бесконечным или ожидает какого-либо условия для завершения.

yield – дорога в обе стороны

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

Всё дело в том, что yield – дорога в обе стороны: он не только возвращает результат наружу, но и может передавать значение извне в генератор.

Вызов let result = yield value делает следующее:

  • Возвращает value во внешний код, приостанавливая выполнение генератора.
  • Внешний код может обработать значение, и затем вызвать next с аргументом: generator.next(arg) .
  • Генератор продолжит выполнение, аргумент next будет возвращён как результат yield (и записан в result ).

Продемонстрируем это на примере:

function* gen() < // Передать вопрос во внешний код и подождать ответа let result = yield "2 + 2?"; alert(result); >let generator = gen(); let question = generator.next().value; // "2 + 2?" setTimeout(() => generator.next(4), 2000);

На рисунке ниже прямоугольником изображён генератор, а вокруг него – «внешний код», который с ним взаимодействует:

На этой иллюстрации показано то, что происходит в генераторе:

  1. Первый вызов generator.next() – всегда без аргумента, он начинает выполнение и возвращает результат первого yield («2+2?»). На этой точке генератор приостанавливает выполнение.
  2. Результат yield переходит во внешний код (в question ). Внешний код может выполнять любые асинхронные задачи, генератор стоит «на паузе».
  3. Когда асинхронные задачи готовы, внешний код вызывает generator.next(4) с аргументом. Выполнение генератора возобновляется, а 4 выходит из присваивания как результат let result = yield . .

В примере выше – только два next .

Увеличим их количество, чтобы стал более понятен общий поток выполнения:

function* gen() < let ask1 = yield "2 + 2?"; alert(ask1); // 4 let ask2 = yield "3 * 3?" alert(ask2); // 9 >let generator = gen(); alert( generator.next().value ); // "2 + 2?" alert( generator.next(4).value ); // "3 * 3?" alert( generator.next(9).done ); // true

Взаимодействие с внешним кодом:

  1. Первый .next() начинает выполнение… Оно доходит до первого yield .
  2. Результат возвращается во внешний код.
  3. Второй .next(4) передаёт 4 обратно в генератор как результат первого yield и возобновляет выполнение.
  4. …Оно доходит до второго yield , который станет результатом .next(4) .
  5. Третий next(9) передаёт 9 в генератор как результат второго yield и возобновляет выполнение, которое завершается окончанием функции, так что done: true .

Получается «пинг-понг»: каждый next(value) передаёт в генератор значение, которое становится результатом текущего yield , возобновляет выполнение и получает выражение из следующего yield . Исключением является первый вызов next , который не может передать значение в генератор, т.к. ещё не было ни одного yield .

generator.throw

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

…Но «вернуть» можно не только результат, но и ошибку!

Для того, чтобы передать в yield ошибку, используется вызов generator.throw(err) . При этом на строке с yield возникает исключение.

Например, в коде ниже обращение к внешнему коду yield «Сколько будет 2 + 2» завершится с ошибкой:

function* gen() < try < // в этой строке возникнет ошибка let result = yield "Сколько будет 2 + 2?"; // (**) alert("выше будет исключение ^^^"); >catch(e) < alert(e); // выведет ошибку >> let generator = gen(); let question = generator.next().value; generator.throw(new Error("ответ не найден в моей базе данных")); // (*)

«Вброшенная» в строке (*) ошибка возникает в строке с yield (**) . Далее она обрабатывается как обычно. В примере выше она перехватывается try..catch и выводится.

Если ошибку не перехватить, то она «выпадет» из генератора. По стеку ближайший вызов, который инициировал выполнение – это строка с .throw . Можно перехватить её там, как и продемонстрировано в примере ниже:

function* gen() < // В этой строке возникнет ошибка let result = yield "Сколько будет 2 + 2?"; >let generator = gen(); let question = generator.next().value; try < generator.throw(new Error("ответ не найден в моей базе данных")); >catch(e) < alert(e); // выведет ошибку >

Если же ошибка и там не перехвачена, то дальше – как обычно, либо try..catch снаружи, либо она «повалит» скрипт.

Плоский асинхронный код

Одна из основных областей применения генераторов – написание «плоского» асинхронного кода.

Общий принцип такой:

  • Генератор yield’ит не просто значения, а промисы.
  • Есть специальная «функция-чернорабочий» execute(generator) которая запускает генератор, последовательными вызовами next получает из него промисы – один за другим, и, когда очередной промис выполнится, возвращает его результат в генератор следующим next .
  • Последнее значение генератора ( done:true ) execute уже обрабатывает как окончательный результат – например, возвращает через промис куда-то ещё, во внешний код или просто использует, как в примере ниже.

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

Для AJAX-запросов будем использовать метод fetch, он как раз возвращает промисы.

// генератор для получения и показа аватара // он yield'ит промисы function* showUserAvatar() < let userFetch = yield fetch('/article/generator/user.json'); let userInfo = yield userFetch.json(); let githubFetch = yield fetch(`https://api.github.com/users/$`); let githubUserInfo = yield githubFetch.json(); let img = new Image(); img.src = githubUserInfo.avatar_url; img.className = "promise-avatar-example"; document.body.appendChild(img); yield new Promise(resolve => setTimeout(resolve, 3000)); img.remove(); return img.src; > // вспомогательная функция-чернорабочий // для выполнения промисов из generator function execute(generator, yieldValue) < let next = generator.next(yieldValue); if (!next.done) < next.value.then( result =>execute(generator, result), err => generator.throw(err) ); > else < // обработаем результат return из генератора // обычно здесь вызов callback или что-то в этом духе alert(next.value); >> execute( showUserAvatar() );

Функция execute в примере выше – универсальная, она может работать с любым генератором, который yield’ит промисы.

Вместе с тем, это – всего лишь набросок, чтобы было понятно, как такая функция в принципе работает. Есть уже готовые реализации, обладающие большим количеством возможностей.

Одна из самых известных – это библиотека co, которую мы рассмотрим далее.

Библиотека «co»

Библиотека co , как и execute в примере выше, получает генератор и выполняет его.

Начнём сразу с примера, а потом – детали и полезные возможности:

co(function*() < let result = yield new Promise( resolve =>setTimeout(resolve, 1000, 1) ); alert(result); // 1 >)

Предполагается, что библиотека co подключена к странице , например, отсюда: https://cdnjs.com/libraries/co/. В примере выше function*() делает yield промиса с setTimeout , который через секунду возвращает 1 .

Вызов co(…) возвращает промис с результатом генератора. Если в примере выше function*() что-то возвратит, то это можно будет получить через .then в результате co :

co(function*() < let result = yield new Promise( resolve =>setTimeout(resolve, 1000, 1) ); return result; // return 1 >).then(alert); // 1

Обязательно нужен catch

Частая ошибка начинающих – вообще забывать про обработку результата co . Даже если результата нет, ошибки нужно обработать через catch , иначе они «подвиснут» в промисе.

Такой код ничего не выведет:

co(function*() < throw new Error("Sorry that happened"); >)

Программист даже не узнает об ошибке. Особенно обидно, когда это опечатка или другая программная ошибка, которую обязательно нужно поправить.

co(function*() < throw new Error("Sorry that happened"); >).catch(alert); // обработать ошибку как-либо

Большинство примеров этого catch не содержат, но это лишь потому, что в примерах ошибок нет. А в реальном коде обязательно нужен catch .

Библиотека co умеет выполнять не только промисы. Есть несколько видов значений, которые можно yield ить, и их обработает co :

  • Промис.
  • Объект-генератор.
  • Функция-генератор function*() – co её выполнит, затем выполнит полученный генератор.
  • Функция с единственным аргументом вида function(callback) – библиотека co её запустит со своей функцией- callback и будет ожидать, что при ошибке она вызовет callback(err) , а при успешном выполнении – callback(null, result) . То есть, в первом аргументе – будет ошибка (если есть), а втором – результат (если нет ошибки). После чего результат будет передан в генератор.
  • Массив или объект из вышеперечисленного. При этом все задачи будут выполнены параллельно, и результат, в той же структуре, будет выдан наружу.

В примере ниже происходит yield всех этих видов значений. Библиотека co обеспечивает их выполнение и возврат результата в генератор:

Object.defineProperty(window, 'result', < // присвоение result=… будет выводить значение set: value =>alert(JSON.stringify(value)) >); co(function*() < result = yield function*() < // генератор return 1; >(); result = yield function*() < // функция-генератор return 2; >; result = yield Promise.resolve(3); // промис result = yield function(callback) < // function(callback) setTimeout(() =>callback(null, 4), 1000); >; result = yield < // две задачи выполнит параллельно, как Promise.all one: Promise.resolve(1), two: function*() < return 2; >>; result = yield [ // две задачи выполнит параллельно, как Promise.all Promise.resolve(1), function*() < return 2 >]; >);

Устаревший yield function(callback)

Отдельно заметим вариант с yield function(callback) . Такие функции, с единственным-аргументом callback’ом, в англоязычной литературе называют «thunk».

Функция обязана выполниться и вызвать (асинхронно) либо callback(err) с ошибкой, либо callback(null, result) с результатом.

Использование таких функций в yield является устаревшим подходом, так как там, где можно использовать yield function(callback) , можно использовать и промисы. При этом промисы мощнее. Но в старом коде его ещё можно встретить.

Посмотрим пример посложнее, с композицией генераторов:

co(function*() < let result = yield* gen(); alert(result); // hello >); function* gen() < return yield* gen2(); >function* gen2() < let result = yield new Promise( // (1) resolve =>setTimeout(resolve, 1000, 'hello') ); return result; >

Это – отличный вариант для библиотеки co . Композиция yield* gen() вызывает gen() в потоке внешнего генератора. Аналогично делает и yield* gen2() .

Поэтому yield new Promise из строки (1) в gen2() попадает напрямую в библиотеку co , как если бы он был сделан так:

co(function*() < // gen() и затем gen2() встраиваются во внешний генератор let result = yield new Promise( resolve =>setTimeout(resolve, 1000, 'hello') ); alert(result); // hello >);

Пример showUserAvatar() можно переписать с использованием co вот так:

// Загрузить данные пользователя с нашего сервера function* fetchUser(url) < let userFetch = yield fetch(url); let user = yield userFetch.json(); return user; >// Загрузить профиль пользователя с github function* fetchGithubUser(user) < let githubFetch = yield fetch(`https://api.github.com/users/$`); let githubUser = yield githubFetch.json(); return githubUser; > // Подождать ms миллисекунд function sleep(ms) < return new Promise(resolve =>setTimeout(resolve, ms)); > // Использовать функции выше для получения аватара пользователя function* fetchAvatar(url) < let user = yield* fetchUser(url); let githubUser = yield* fetchGithubUser(user); return githubUser.avatar_url; >// Использовать функции выше для получения и показа аватара function* showUserAvatar() < let avatarUrl; try < avatarUrl = yield* fetchAvatar('/article/generator/user.json'); >catch(e) < avatarUrl = '/article/generator/anon.png'; >let img = new Image(); img.src = avatarUrl; img.className = "promise-avatar-example"; document.body.appendChild(img); yield sleep(2000); img.remove(); return img.src; > co(showUserAvatar);

Заметим, что для перехвата ошибок при получении аватара используется try..catch вокруг yield* fetchAvatar :

try < avatarUrl = yield* fetchAvatar('/article/generator/user.json'); >catch(e) < avatarUrl = '/article/generator/anon.png'; >

Это – одно из главных удобств использования генераторов. Несмотря на то, что операции fetch – асинхронные, мы можем использовать обычный try..catch для обработки ошибок в них.

Для генераторов – только yield*

Библиотека co технически позволяет писать код так:

let user = yield fetchUser(url); // вместо // let user = yield* fetchUser(url);

То есть, можно сделать yield генератора, co() его выполнит и передаст значение обратно. Как мы видели выше, библиотека co – довольно всеядна. Однако, рекомендуется использовать для вызова функций-генераторов именно yield* .

Причин для этого несколько:

  1. Делегирование генераторов yield* – это встроенный механизм JavaScript. Вместо возвращения значения обратно в co , выполнения кода библиотеки… Мы просто используем возможности языка. Это правильнее.
  2. Поскольку не происходит лишних вызовов, это быстрее по производительности.
  3. И, наконец, пожалуй, самое приятное – делегирование генераторов сохраняет стек.

Проиллюстрируем последнее на примере:

co(function*() < // при запуске в стеке не будет видно этой строки yield g(); // (*) >).catch(function(err) < alert(err.stack); >); function* g()

Зачем они нужны в JavaScript? Symbol, Iterator, Generator

Кратко, просто и понятно, что это такое и как это применять.

Начнём с Symbol

Что это?

Это новый примитивный тип данных, как boolean или string , который нужен для создания уникальных идентификаторов. Для простоты понимания представьте себе функцию которая возвращает всегда уникальный id, только в нашем случае вместо id – символ.

Пока всё просто. Теперь давайте посложнее.

Вот тут symbol становится немного странным, все другие примитивы могут стать объектами, если объявить их через new , а symbol не может. Почему так?

Это удерживает разработчиков от создания явного объекта-обёртки Symbol вместо нового символьного значения. Создание явных объектов-обёрток для примитивных типов доступно (например, new Boolean, new String, new Number).

Простыми словами — от символа мы хотим получить уникальный идентификатор, то есть сам Символ, а new возвращает объект, объект нам не нужен.

И последнее про Symbol

Существуют «глобальные символы», они доступны во всех частях вашей программы. То есть вы можете создать символ и поместить его в некую базу, это делается с помощью функции Symbol.for()

Если мы ещё раз вызовем Symbol.for(“Kanye West”) он вернёт существующий символ, а не новый.

Теперь к главному.

Зачем нужен Symbol ?

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

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

Для решение этой проблемы, можно использовать symbol

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

Ну окей. Представим, вы вдруг решили добавить свойство __my_uniq_logs__ , вместо symbol , тогда смотрим что произойдёт дальше.

  1. При вызове метода Object.keys(myObj) вернётся дополнительное поле — ваши логи (а Object.keys обычно используется для прохода по массиву ключей)
  2. Новая не предвиденная переменная (ваши логи) в этом массиве ломает приложение

Так что, Symbol — ваш друг.

Также есть и более замороченные применения, про них можно почитать тут https://www.keithcirkel.co.uk/metaprogramming-in-es6-symbols/

Iterator

Что это?

В JavaScript есть так называемые итерируемые объекты, то есть объекты содержимое которых мы можем перебрать. Как например массив. Сам перебор, в большинстве своём, осуществляется с помощью итераторов (к примеру конструкция for..of для перебора элементов использует итераторы)

Давайте сделаем один.

А теперь используем наш итератор.

Что получается, итератор — это объект, который предоставляет метод next() , возвращающий следующий элемент последовательности.

Теперь более реальный пример.

У нас есть объект, который нужно «умно» перебрать.

Для того чтобы for..of выводил то, что мы хотим, нужно сделать объект range итерируемым.

Дальше без паники, всё объясню.

Symbol.iterator — что это ?

Как я говорил выше, конструкция for..of использует итераторы для перебора данных. В начале своего выполнения, for..of автоматически вызывает Symbol.iterator() для получения итератора и далее вызывает метод next() до получения < done: true >. Это внутренняя механика JavaScript, так уж он работает.

Зачем нужен Iterator ?

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

Generator

Что это ?

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

Так выглядят генераторы.

В общем, это обычная функция перед которой стоит *

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

Вот как это работает на практике

Yield как бы говорит — передаём name и ставим паузу, пока не произойдёт следующий вызов next()

Также yield может принимать значения из вне.

Разберём код выше типичной ситуацией из жизни.

Представим что наш генератор это Канье Уэст который лично привёз вам домой свой альбом. Когда происходит первый вызов myIterator.next() Канье заходит к вам в дом и дарит свой альбом (передается name ). Канье человек простой, он хочет услышать в ответ благодарность и готов ждать её хоть вечность.

Вот, вы послушали альбом, подходите к Канье и говорите Спасибо (с помощью myIterator.next(‘West’) передаётся ‘West’ обратно в функцию), Канье принимает благодарность (переменной who присваивается ‘West’ ) и тут же уходит по своим делам. Такой уж Канье.

Зачем нужен Generator ?

В целом, его используют разные библиотеки как замену async/await для работы с асинхронными операциями. Сам async/await кстати, это high level абстракция над генераторами.

К примеру с помощью библиотеки co можно убрать callback hell и не использовать async/await .

Также популярная redux-saga использует генераторы.

В остальных случаях их применяют очень редко.

Суммируем

Symbols — новый уникальный тип данных. Обычно используется как свойство объекта, чтобы не поломать Object.keys и for-in .

Iterators — объект, который предоставляет метод next() , возвращающий следующий элемент последовательности. Обычно используется для кастомного перебора значений объектов в for-of и … (spread operator).

Generators — hight level абстракция над итераторами. Обычно используется как низкоуровневая альтернатива async/await .

И напоследок

Сегодня страшное стало понятным….слегка.

Если вы используете symbol, iterator и generator в своих проектах как-то по другому, пишите в комментариях, обязательно добавлю.

Если интересно как надо писать на React, то ниже есть гайд.

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

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