// Tutorial //

Понимание генераторов в JavaScript

Published on March 19, 2020
Default avatar
By Tania Rascia
Developer and author at DigitalOcean.
Понимание генераторов в JavaScript

Автор выбрал фонд 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 и зарегистрирована в консоли.

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

В следующей таблице представлен перечень методов, которые можно использовать на объектах 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)
}

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

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.


Want to learn more? Join the DigitalOcean Community!

Join our DigitalOcean community of over a million developers for free! Get help and share knowledge in our Questions & Answers section, find tutorials and tools that will help you grow as a developer and scale your project or business, and subscribe to topics of interest.

Sign up
About the authors
Default avatar
Developer and author at DigitalOcean.

Default avatar
Senior Technical Editor

Editor at DigitalOcean, fiction writer and podcaster elsewhere, always searching for the next good nautical pun!


Still looking for an answer?

Was this helpful?
Leave a comment