Автор выбрал фонд Open Internet/Free Speech для получения пожертвования в рамках программы Write for DOnations.
Введение
В ECMAScript 2015 были введены генераторы для языка JavaScript. Генератор — это процесс, который может быть остановлен и возобновлен, и может выдать несколько значений. Генаратор в JavaScript состоит из функции генераторов, которая возвращает элемент Generator
, поддерживающий итерации.
Генераторы могут поддерживать состояние и обеспечивать эффективный способ создания итераторов, а также позволяют работать с бесконечным потоком данных, который можно использовать для установки бесконечной прокрутки на внешнем интерфейсе веб-приложений, для работы с данными звуковой волны и т. д. Кроме того, при использовании Promises генераторы могут имитировать функцию async/await
, которая позволяет работать с асинхронным кодом более простым и читаемым способом. Хотя async/await
является более распространенным способом работы с асинхронными вариантами использования, например извлечения данных из API, генераторы обладают более усовершенствованными функциями, что абсолютно оправдывает изучение методов их использования.
В этой статье мы расскажем, как создавать функции-генераторы, выполнять итеративный обход объектов Generator
, объясним разницу между yield
и return
внутри генератора, а также коснемся других аспектов работы с генераторами.
Функции-генераторы
Функция-генератор — это функция, которая возвращает объект генератора
и определяется по ключевому слову функции
, за которым следует звездочка (*
), как показано ниже:
// 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()
Если бы это была штатная функция, мы бы могли ожидать, что генератор
даст нам строку, переданную в функцию. Однако фактически мы получаем элемент в приостановленном
состоянии. Таким образом, вызов генератора
даст результат, аналогичный следующему:
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()
Результат будет выглядеть следующим образом:
Output
{value: "Hello, Generator!", done: true}
Вызов next()
возвращает значение Hello, Generator!
, а состояние done
имеет значение true
, так как это значение произошло из return
, что закрыло итератор. Поскольку итератор выполнен, статус функции генератора будет изменен с suspended
на closed
. Повторный вызов генератора
даст следующее:
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()
В результате будут выведены следующие четыре строки по порядку:
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)
}
В результате будет получено следующее:
Output
Neo
Morpheus
Trinity
Оператор расширения также может быть использован для присвоения значений Generator
для массива.
// Create an array from the values of a Generator object
const values = [...generator]
console.log(values)
Это даст следующий массив:
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()
Будет получено три следующих результата:
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!'))
Результат будет выглядеть следующим образом:
Output
{value: "Neo", done: false}
Error: Agent Smith!
{value: undefined, done: true}
С помощью throw()
, мы ввели ошибку в генератор, которая была перехвачена try...catch
и зарегистрирована в консоли.
Методы и состояния объекта генератора
В следующей таблице представлен перечень методов, которые можно использовать на объектах Generato
r:
Метод | Описание |
---|---|
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)
}
Это даст следующие значения в порядке их генерирования:
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()
Результат будет выглядеть следующим образом:
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)
}
В результате вы получите следующий вывод:
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)
Результат будет выглядеть следующим образом:
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()
и создадим новое значение в следующей итерации, которое является предыдущим значением,умноженным на десять. В результате вы получите следующий вывод:
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, аналогичные следующим:
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
:
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.