Генераторы в JavaScript

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

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

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

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

Функция-генератор – это функция, которая возвращает объект Generator. Она определяется ключевым словом function со звездочкой (*).

// Generator function declaration
function* generatorFunction() {}

Иногда звездочка ставится рядом с именем функции, а не с ключевым словом function, например: 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() {}
}

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

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

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

Объекты Generator

Традиционно функции в 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()

Если бы это была обычная функция, generator выдал бы строку, возвращенную в функции. Однако мы получаем объект в состоянии suspended. Поэтому вызов generator выдаст подобный вывод:

generatorFunction {<suspended>}
__proto__: Generator
[[GeneratorLocation]]: VM272:1
[[GeneratorStatus]]: "suspended"
[[GeneratorFunction]]: ƒ* generatorFunction()
[[GeneratorReceiver]]: Window
[[Scopes]]: Scopes[3]

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

Зная все это, давайте попробуем вызвать next() для generator и получить текущее значение и состояние итератора:

// Call the next method on the Generator object
generator.next()

Это выведет на экран следующий вывод:

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

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

generatorFunction {<closed>}

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

Операторы yield

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

Давайте для примера трижды приостановим функцию-генератор с разными значениями и в конце выведем значение на экран. Затем мы присвоим объект Generator переменной 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()

Это выведет следующие четыре строки по порядку:

{value: "Neo", done: false}
{value: "Morpheus", done: false}
{value: "Trinity", done: false}
{value: "The Oracle", done: true}

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

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

Читайте также:

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

// Iterate over Generator object
for (const value of generator) {
console.log(value)
}

Это вернет следующий вывод:

Neo
Morpheus
Trinity

Также для присвоения значений объекта Generator массиву можно использовать оператор spread.

// Create an array from the values of a Generator object
const values = [...generator]
console.log(values)

У вас получится следующий массив:

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

Как spread, так и for…of не будет учитывать return (в данном случае это было бы значение ‘The Oracle’).

Примечание: Оба эти метода эффективно работают с конечными генераторами. Но если генератор имеет дело с бесконечным потоком данных, spread и 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 и done со значением false. Если мы сразу же после этого вызовем метод return() для объекта Generator, значение будет передано, и done будет true. Любой дополнительный вызов next() выдаст стандартный ответ выполненного генератора с неопределенным значением.

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

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

Это даст три результата:

{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!'))

На экране появится следующий вывод:

{value: "Neo", done: false}
Error: Agent Smith!
{value: undefined, done: true}

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

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

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

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

А в этой таблице собраны состояния объекта Generator.

Состояние Описание
suspended Генератор остановился, но не прекратил работу.
closed Генератор завершил работу, обнаружив ошибку, ключевое слово return или перебрав все значения.

Делегирование оператора 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)
}

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

1
2
3
4

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

Оператор yield* также может делегировать значения любому итерируемому объекту, например, массиву или Map. Делегирование yield поможет улучшить организацию кода (поскольку любая функция в генераторе, которая хочет использовать yield, также должна быть генератором).

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

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

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

// Define a generator function that increments by one
function* incrementer() {
let i = 0
while (true) {
yield i++
}
}
// Initiate the generator
const counter = incrementer()

Теперь выполните итерацию по значениям с помощью next():

counter.next()
counter.next()
counter.next()
counter.next()

Вы получите такой вывод:

{value: 0, done: false}
{value: 1, done: false}
{value: 2, done: false}
{value: 3, done: false}

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

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

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

// 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)
}

Получится такой результат:

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)

Вы получите такой результат:

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() и передадим новое значение (то есть предыдущее значение, умноженное на десять) в следующую итерацию. Получится следующее:

0
10
20
30
40

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

async/await и генераторы

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

Для примера давайте создадим асинхронную функцию, которая использует 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:

[ {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, за исключением того, что тут передается функция-генератор, которая возвращает значения.

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

// 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:

[ {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, но она не используется широко.

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

Читайте также:

Tags:

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