Автор выбрал фонд 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()
Если бы это была штатная функция, мы бы могли ожидать, что генератор
даст нам строку, переданную в функцию. Однако фактически мы получаем элемент в приостановленном
состоянии. Таким образом, вызов генератора
даст результат, аналогичный следующему:
OutputgeneratorFunction {<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
. Повторный вызов генератора
даст следующее:
OutputgeneratorFunction {<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)
}
В результате будет получено следующее:
OutputNeo
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)
}
Это даст следующие значения в порядке их генерирования:
Output1
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)
}
В результате вы получите следующий вывод:
Output0
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)
Результат будет выглядеть следующим образом:
Output100
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()
и создадим новое значение в следующей итерации, которое является предыдущим значением,умноженным на десять. В результате вы получите следующий вывод:
Output0
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.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!