URL: https://www.progressiverobot.com/understanding-generators-in-javascript-ru/

*Автор выбрал фонд Open Internet/Free Speech для получения пожертвования в рамках программы Write for DOnations.*

Введение

В ECMAScript 2015 были введены генераторы для языка JavaScript. *Генератор* — это процесс, который может быть остановлен и возобновлен, и может выдать несколько значений. Генаратор в JavaScript состоит из функции генераторов, которая возвращает элемент Generator, поддерживающий итерации.

Генераторы могут поддерживать состояние и обеспечивать эффективный способ создания итераторов, а также позволяют работать с бесконечным потоком данных, который можно использовать для установки бесконечной прокрутки на внешнем интерфейсе веб-приложений, для работы с данными звуковой волны и т. д. Кроме того, при использовании Promises генераторы могут имитировать функцию async/await, которая позволяет работать с асинхронным кодом более простым и читаемым способом. Хотя async/await является более распространенным способом работы с асинхронными вариантами использования, например извлечения данных из API, генераторы обладают более усовершенствованными функциями, что абсолютно оправдывает изучение методов их использования.

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

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

javascript illustration for: Функции-генераторы

Функция-генератор — это функция, которая возвращает объект генератора и определяется по ключевому слову функции, за которым следует звездочка (*), как показано ниже:

				
					
// Generator function declaration

function* generatorFunction() {}

				
			

Иногда звездочка отображается рядом с названием функции напротив ключевого слова, например function *generatorFunction(). Это работает так же, но функция со звездочкой function* является более распространенной синтаксической конструкцией.

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

				
					
// Generator function expression

const generatorFunction = function*() {}

				
			

Генераторы могут даже быть методами объекта или класса:

				
					
// Generator as the method of an object

const generatorObj = {

 *generatorMethod() {},

}



// Generator as the method of a class

class GeneratorClass {

 *generatorMethod() {}

}

				
			

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

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

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

Объекты генератора

Обычно функции в JavaScript выполняются до завершения, и вызов функции вернет значение, когда она дойдет до ключевого слова return. Если пропущено ключевое слово ​​​return, функция вернет значение undefined.

Например, в следующем коде мы декларируем функцию sum(), которая возвращает значение, состоящее из суммы двух целых аргументов:

				
					
// A regular function that sums two values

function sum(a, b) {

 return a + b

}

				
			

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

				
					
const value = sum(5, 6) // 11

				
			

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

				
					
// Declare a generator function with a single return value

function* generatorFunction() {

 return 'Hello, Generator!'

}

				
			

Активация функции генератора возвращает элемент Generator, который мы можем отнести к переменной:

				
					
// Assign the Generator object to generator

const generator = generatorFunction()

				
			

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

				
					
[secondary_label Output]

generatorFunction {<suspended>}

 __proto__: Generator

 [[GeneratorLocation]]: VM272:1

 [[GeneratorStatus]]: "suspended"

 [[GeneratorFunction]]: ƒ* generatorFunction()

 [[GeneratorReceiver]]: Window

 [[Scopes]]: Scopes[3]

				
			

Элемент Generator, возвращаемый функцией — это итератор. *Итератор* — это объект, имеющий метод ​​​​​​next()​​​, который используется для итерации последовательности значений. Метод next() возвращает элемент со свойствами value и done. value означает возвращаемое значение, а done указывает, прошел ли итератор все свои значения или нет.

Зная это, давайте вызовем функцию next() нашего генератора и получим текущее значение и состояние итератора:

				
					
// Call the next method on the Generator object

generator.next()

				
			

Результат будет выглядеть следующим образом:

				
					
[secondary_label Output]

{value: "Hello, Generator!", done: true}

				
			

Вызов next() возвращает значение Hello, Generator!, а состояние done имеет значение true, так как это значение произошло из return, что закрыло итератор. Поскольку итератор выполнен, статус функции генератора будет изменен с suspended на closed. Повторный вызов генератора даст следующее:

				
					
[secondary_label Output]

generatorFunction {<closed>}

				
			

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

Операторы yield

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

В этом примере мы остановим функцию генератора три раза с помощью разных значений и вернем значение в конце. Затем мы назначим наш объект Generator для переменной генератора.

				
					
// Create a generator function with multiple yields

function* generatorFunction() {

 yield 'Neo'

 yield 'Morpheus'

 yield 'Trinity'



 return 'The Oracle'

}



const generator = generatorFunction()

				
			

Сейчас, когда мы вызываем next()​​​​​ в функции генератора, она будет останавливаться каждый раз, когда будет встречать yield. done будет устанавливаться для false​​​ после каждого yield, указывая на то, что генератор не завершен. Когда она встретит return или в функции больше не будет yield, done переключится на true, и генератор будет завершен.

Используйте метод next() четыре раза в строке:

				
					
// Call next four times

generator.next()

generator.next()

generator.next()

generator.next()

				
			

В результате будут выведены следующие четыре строки по порядку:

				
					
[secondary_label Output]

{value: "Neo", done: false}

{value: "Morpheus", done: false}

{value: "Trinity", done: false}

{value: "The Oracle", done: true}

				
			

Обратите внимание, что для генератора не требуется return. В случае пропуска последняя итерация вернет {value: undefined, done: true}​​​, по мере наличия последующих вызовов next() после завершения генератора.

Итерация по генератору

С помощью метода next() мы вручную выполнили итерацию объекта Generator​​​, получив все свойства value​​​ и done всего объекта. Однако, как и Array,Map и Set, Generator следует протоколу итерации и может быть итерирован с for...of:

				
					
// Iterate over Generator object

for (const value of generator) {

 console.log(value)

}

				
			

В результате будет получено следующее:

				
					
[secondary_label Output]

Neo

Morpheus

Trinity

				
			

Оператор расширения также может быть использован для присвоения значений Generator​​​ для массива.

				
					
// Create an array from the values of a Generator object

const values = [...generator]



console.log(values)

				
			

Это даст следующий массив:

				
					
[secondary_label Output]

(3) ["Neo", "Morpheus", "Trinity"]

				
			

Как расширение, так и for...of​​​ не разложит return на значения (в этом случае было бы «The Oracle»).

Примечание. Хотя оба эти метода эффективны для работы с конечными генераторами, если генератор работает с бесконечным потоком данных, невозможно будет использовать расширение или for...of​​​ напрямую без создания бесконечного цикла.

Завершение работы генератора

Как мы увидели, генератор может настроить свое свойство done​​​ на true, а статус на closed путем итерации всех своих значений. Немедленно отменить действие генератора можно еще двумя способами: с помощью метода return() и метода throw().

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

Чтобы продемонстрировать return(), мы создадим генератор с несколькими значениями yield, но без return в определении функции:

				
					
function* generatorFunction() {

 yield 'Neo'

 yield 'Morpheus'

 yield 'Trinity'

}



const generator = generatorFunction()

				
			

Первый next() даст нам «Neo» c done установленным на false​​​. Если мы обратимся к методу return()​​​ на объекте Generator сразу после этого, мы получим переданное значение, и done будет установлено на true. Все дополнительные вызовы next() дадут завершенный ответ генератора по умолчанию с неопределенным значением.

Чтобы продемонстрировать это, запустите следующие три метода на генераторе:

				
					
generator.next()

generator.return('There is no spoon!')

generator.next()

				
			

Будет получено три следующих результата:

				
					
[secondary_label Output]

{value: "Neo", done: false}

{value: "There is no spoon!", done: true}

{value: undefined, done: true}

				
			

Метод return() заставил объект Generator завершить работу и проигнорировать все другие ключевые слова yield. Это особенно полезно в асинхронном программировании, когда необходимо, чтобы была возможность отмены для функции, например в случае прерывания веб-запроса, когда пользователь хочет выполнить другое действие, так как невозможно напрямую отменить Promise.

Если тело функции генератора может перехватывать ошибки и работать с ними, можно использовать метод throw() для перебрасывания ошибки в генератор. Это действие запустит генератор, перебросит в него ошибку и прекратит работу генератора.

Чтобы продемонстрировать это, мы поместим try...catch​​​ в тело функции генератора и зарегистрируем ошибку при ее наличии:

				
					
// Define a generator function with a try...catch

function* generatorFunction() {

 try {

 yield 'Neo'

 yield 'Morpheus'

 } catch (error) {

 console.log(error)

 }

}



// Invoke the generator and throw an error

const generator = generatorFunction()

				
			

Теперь мы запустим метод next()​​, за которым последует throw():

				
					
generator.next()

generator.throw(new Error('Agent Smith!'))

				
			

Результат будет выглядеть следующим образом:

				
					
[secondary_label Output]

{value: "Neo", done: false}

Error: Agent Smith!

{value: undefined, done: true}

				
			

С помощью throw(), мы ввели ошибку в генератор, которая была перехвачена try...catch и зарегистрирована в консоли.

Методы и состояния объекта генератора

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

Метод Описание
———————————————————— ————————————————————
next() Возвращает следующее значение генератора
return() Возвращает значение генератора и прекращает работу генератора
throw() Выдает ошибку и прекращает работу генератора

В следующей таблице перечислены возможные состояния объекта Generator:

Состояние Описание
———– ————————————————————
suspended Генератор остановил выполнение, но не прекратил работу
closed Генератор прекратил выполнение из-за обнаружения ошибки, возвращения или итерации всех значений

yield делегирование

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

Для демонстрации мы можем создать две функции генератора, одна из которых будет yield* оператором для другой:

				
					
// Generator function that will be delegated to

function* delegate() {

 yield 3

 yield 4

}



// Outer generator function

function* begin() {

 yield 1

 yield 2

 yield* delegate()

}

				
			

Далее, давайте проведем итерацию посредством функции begin():

				
					
// Iterate through the outer generator

const generator = begin()



for (const value of generator) {

 console.log(value)

}

				
			

Это даст следующие значения в порядке их генерирования:

				
					
[secondary_label Output]

1

2

3

4

				
			

Внешний генератор выдал значения 1 и 2, затем делегировал другому генератору с yield*, который вернул 3 и 4.

yield* также может делегировать любому итерируемому объекту, например Array или Map. Yield делегирование может быть полезным для организации кода, поскольку любая функция в рамках генератора, использующая yield, также должна быть генератором.

Бесконечный поток данных

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

В следующем коде мы определяем функцию генератора и затем запускаем генератор:

				
					
// Define a generator function that increments by one

function* incrementer() {

 let i = 0



 while (true) {

 yield i++

 }

}



// Initiate the generator

const counter = incrementer()

				
			

Затем проводим итерацию значений с использованием next():

				
					
// Iterate through the values

counter.next()

counter.next()

counter.next()

counter.next()

				
			

Результат будет выглядеть следующим образом:

				
					
[secondary_label Output]

{value: 0, done: false}

{value: 1, done: false}

{value: 2, done: false}

{value: 3, done: false}

				
			

Функция возвращает последовательные значения в бесконечном цикле, в то время как свойство done остается false, обеспечивая незавершенность.

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

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

				
					
// Create a fibonacci generator function

function* fibonacci() {

 let prev = 0

 let next = 1



 yield prev

 yield next



 // Add previous and next values and yield them forever

 while (true) {

 const newVal = next + prev



 yield newVal



 prev = next

 next = newVal

 }

}

				
			

Для тестирования мы можем создать цикл конечного числа и напечатать последовательность Фибоначчи в консоль.

				
					
// Print the first 10 values of fibonacci

const fib = fibonacci()



for (let i = 0; i < 10; i++) {

 console.log(fib.next().value)

}

				
			

В результате вы получите следующий вывод:

				
					
[secondary_label Output]

0

1

1

2

3

5

8

13

21

34

				
			

Способность работать с бесконечными наборами данных — это одно из свойств, благодаря которым генераторы являются таким мощным инструментом. Эта способность может использоваться, например для установки бесконечной прокрутки на внешнем интерфейсе веб-приложений.

Передача значений в генераторы

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

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

				
					
function* generatorFunction() {

 console.log(yield)

 console.log(yield)



 return 'The end'

}



const generator = generatorFunction()



generator.next()

generator.next(100)

generator.next(200)

				
			

Результат будет выглядеть следующим образом:

				
					
[secondary_label Output]

100

200

{value: "The end", done: true}

				
			

Также возможно создать генератор с первоначальным значением. В следующем примере мы создадим цикл for и передадим каждое значение в метод next(), но также передадим аргумент в первоначальную функцию:

				
					
function* generatorFunction(value) {

 while (true) {

 value = yield value * 10

 }

}



// Initiate a generator and seed it with an initial value

const generator = generatorFunction(0)



for (let i = 0; i < 5; i++) {

 console.log(generator.next(i).value)

}

				
			

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

				
					
[secondary_label Output]

0

10

20

30

40

				
			

Другой способ запуска генератора — завернуть генератор в функцию, которая всегда будет вызывать next() перед тем, как делать что-либо другое.

async/await в генераторах

Асинхронная функция — вид функции, имеющийся в ES6+ JavaScript, которая облегчает работу с асинхронными данными, делая их синхронными. Генераторы обладают более широким спектром возможностей, чем асинхронные функции, но способны воспроизводить аналогичное поведение. Реализация асинхронного программирования таким образом может повысить гибкость вашего кода.

В этом разделе мы продемонстрируем пример воспроизведения async/await с генераторами.

Давайте создадим асинхронную функцию, которая использует Fetch API для получения данных из JSONPlaceholder API (дает пример данных JSON для тестирования) и регистрирует ответ в консоли.

Для начала определим асинхронную функцию под названием getUsers, которая получает данные из API и возвращает массив объектов, затем вызовем getUsers:

				
					
const getUsers = async function() {

 const response = await fetch('https://jsonplaceholder.typicode.com/users')

 const json = await response.json()



 return json

}



// Call the getUsers function and log the response

getUsers().then(response => console.log(response))

				
			

Это даст данные JSON, аналогичные следующим:

				
					
[secondary_label Output]

[ {id: 1, name: "Leanne Graham" ...},

 {id: 2, name: "Ervin Howell" ...},

 {id: 3, name": "Clementine Bauch" ...},

 {id: 4, name: "Patricia Lebsack"...},

 {id: 5, name: "Chelsey Dietrich"...},

 ...]

				
			

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

В следующем блоке кода мы определим функцию под названием getUsers, которая использует нашу новую функцию asyncAlt (будет описана позже) для имитации async/await.

				
					
const getUsers = asyncAlt(function*() {

 const response = yield fetch('https://jsonplaceholder.typicode.com/users')

 const json = yield response.json()



 return json

})



// Invoking the function

getUsers().then(response => console.log(response))

				
			

Как мы видим, она выглядит почти идентично реализации async/await, за исключением того, что имеется функция генератора, которая передается в этих значениях функции yield.

Теперь мы можем создать функцию asyncAlt, которая напоминает асинхронную функцию. asyncAlt​​​ имеет функцию генератора в качестве параметра и является нашей функцией, вырабатывающей промисы, которые получают возвраты. asyncAlt​​​ возвращает непосредственно функцию и решает каждый найденный промис до последнего:

				
					
// Define a function named asyncAlt that takes a generator function as an argument

function asyncAlt(generatorFunction) {

 // Return a function

 return function() {

 // Create and assign the generator object

 const generator = generatorFunction()



 // Define a function that accepts the next iteration of the generator

 function resolve(next) {

 // If the generator is closed and there are no more values to yield,

 // resolve the last value

 if (next.done) {

 return Promise.resolve(next.value)

 }



 // If there are still values to yield, they are promises and

 // must be resolved.

 return Promise.resolve(next.value).then(response => {

 return resolve(generator.next(response))

 })

 }



 // Begin resolving promises

 return resolve(generator.next())

 }

}

				
			

Это даст тот же результат, что и в версии async/await:

				
					
[secondary_label Output]

[ {id: 1, name: "Leanne Graham" ...},

 {id: 2, name: "Ervin Howell" ...},

 {id: 3, name": "Clementine Bauch" ...},

 {id: 4, name: "Patricia Lebsack"...},

 {id: 5, name: "Chelsey Dietrich"...},

 ...]

				
			

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

Заключение

Генераторы — это процессы, которые могут останавливать и возобновлять выполнение. Они являются мощной, универсальной, хотя и не слишком распространенной функцией JavaScript. В данном учебном пособии мы узнали о функциях и объектах генератора, методах, доступных для генераторов, операторах yield и yield*, а также генераторах, используемых с конечными и бесконечными массивами данных. Мы также изучили один способ реализации асинхронного кода без вложенных обратных вызовов или длинных цепочек промисов.

Если вы хотите узнать больше о синтаксисе JavaScript, ознакомьтесь с учебными пособиями Понимание методов This, Bind, Call и Apply в JavaScript​​​ и Понимание объектов Map и Set в JavaScript.