了解 JavaScript 中的生成器

作者选择了 开放式互联网 / 自由言论基金作为 写给捐赠计划的一部分接受捐款。

介绍

ECMAScript 2015中,引入了 JavaScript 语言的生成器。 generator 是一个可以暂停和恢复的过程,可以产生多个值。

发电机可以维持状态,提供一种高效的方式来制造迭代器,并且能够处理无限数据流,可用于在 Web 应用程序的前端实现无限滚动,以声音波数据操作等等。此外,当与 Promises使用时,发电机可以模仿async/await功能,这使我们能够以更简单和可读的方式处理 asynchronous code

在本文中,我们将涵盖如何创建发电机函数,如何重复发电机对象,发电机内部的收益回报之间的差异,以及与发电机工作的其他方面。

发电机功能

生成函数是返回一个生成对象的函数,并由函数的关键字,然后是星座(*),如下所示:

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

有时,您会看到函数名称旁边的星座,而不是函数关键字,例如函数 *generatorFunction()

生成器函数也可以在一个表达式中定义,例如常规 函数:

1// Generator function expression
2const generatorFunction = function*() {}

发电机甚至可以是 objectclass的方法:

1// Generator as the method of an object
2const generatorObj = {
3  *generatorMethod() {},
4}
5
6// Generator as the method of a class
7class GeneratorClass {
8  *generatorMethod() {}
9}

本文中的示例将使用生成器函数声明语法。

<$>[注] :与常规函数不同,发电器不能用关键字构建,也不能与 箭头函数一起使用。

现在你知道如何声明发电机函数,让我们看看它们返回的可重复的发电机对象。

物体发电机

传统上,JavaScript中的函数运行到完成,呼叫函数将返回值,当它到达返回关键字时。

例如,在下面的代码中,我们声明一个‘sum()’函数,该函数返回一个值,该值是两个整数参数的总和:

1// A regular function that sums two values
2function sum(a, b) {
3  return a + b
4}

调用函数返回一个值,该值是参数的总和:

1sum(5, 6) // 11

然而,生成函数不会立即返回值,而是返回可迭代的生成对象。

1// Declare a generator function with a single return value
2function* generatorFunction() {
3  return 'Hello, Generator!'
4}

当我们调用发电函数时,它会返回 Generator对象,我们可以将其分配给一个变量:

1// Assign the Generator object to generator
2const generator = generatorFunction();

如果这是一个常规的函数,我们会期望发电机给我们函数中返回的字符串,但是,我们实际上得到的是处于暂停状态的对象,因此呼叫发电机会产生类似于以下的输出:

1[secondary_label Output]
2generatorFunction {<suspended>}
3  __proto__: Generator
4  [[GeneratorLocation]]: VM272:1
5  [[GeneratorStatus]]: "suspended"
6  [[GeneratorFunction]]: ƒ* generatorFunction()
7  [[GeneratorReceiver]]: Window
8  [[Scopes]]: Scopes[3]

函数返回的生成器对象是 iteratoriterator 是具有可用的next()方法的对象,用于通过值序列进行迭代。

知道这一点,让我们在我们的发电机上呼叫next(),并获得迭代器的当前值和状态:

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

这将产生以下产出:

1[secondary_label Output]
2{value: "Hello, Generator!", done: true}

调用next()时返回的值为Hello, Generator!完成状态为true,因为这个值来自关闭 iterator 的返回

1[secondary_label Output]
2generatorFunction {<closed>}

截至目前,我们只展示了发电机函数是如何获得函数的回报值的更复杂方法,但发电机函数也有独特的特性,使其与正常函数区分开来。

盈利运营商

发电机在JavaScript中引入一个新的关键字: yieldyield 可以暂停发电机函数并返回随后的值 yield,提供轻量级的方式来迭代值。

在本示例中,我们将将发电机函数暂停三次,以不同的值,然后在末尾返回一个值,然后我们将我们的发电机对象分配给发电机变量。

 1// Create a generator function with multiple yields
 2function* generatorFunction() {
 3  yield 'Neo'
 4  yield 'Morpheus'
 5  yield 'Trinity'
 6
 7  return 'The Oracle'
 8}
 9
10const generator = generatorFunction()

现在,当我们在发电机函数上调用‘next()’时,它每次遇到‘yield’时都会停下来。‘done’将在每个‘yield’后设置为‘false’,表示发电机尚未完成。

使用next()方法连续四次:

1// Call next four times
2generator.next()
3generator.next()
4generator.next()
5generator.next()

这些将为以下四个输出线提供顺序:

1[secondary_label Output]
2{value: "Neo", done: false}
3{value: "Morpheus", done: false}
4{value: "Trinity", done: false}
5{value: "The Oracle", done: true}

请注意,发电机不需要返回;如果被忽略,最后一次迭代将返回{值:未定义,完成: true},以及发电机完成后任何后续调用next()

超越发电机

使用‘next()’方法,我们通过‘Generator’对象手动迭代,接收了整个对象的所有‘值’和‘完成’属性,然而,就像 ‘Array’, ‘Map’和‘Set’一样,一个‘Generator’遵循 iteration protocol,并且可以通过 `for...of进行迭代:

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

这将返回如下:

1[secondary_label Output]
2Neo
3Morpheus
4Trinity

散布运算器也可以用来分配一个数组的生成器的值。

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

这将为下列数组提供:

1[secondary_label Output]
2(3) ["Neo", "Morpheus", "Trinity"]

散布和为...of都不会将回报纳入值中(在这种情况下,它将是The Oracle)。

<$>[注] :虽然这两种方法在与有限生成器工作时都有效,但如果生成器处理的是无限数据流,则无法直接使用 Spread 或for...of 而无需创建无限循环。

关闭发电机

正如我们所看到的,发电机可以将其完成属性设置为真实并将其状态设置为关闭,通过重复其所有值。

使用return(),生成器可以随时终止,就像一个return语句存在于函数体内一样。

要演示 return(),我们将创建一个产生器,在函数定义中有几个 yield 值,但没有 return:

1function* generatorFunction() {
2  yield 'Neo'
3  yield 'Morpheus'
4  yield 'Trinity'
5}
6
7const generator = generatorFunction()

第一個「next()」將給我們「Neo」,而「完成」設定為「false」。如果我們在「Generator」對象上立即召喚一個「return()」方法,我們現在就會得到通過的值,並且「完成」設定為「true」。

要证明这一点,在发电机上运行以下三种方法:

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

这将带来以下三个结果:

1[secondary_label Output]
2{value: "Neo", done: false}
3{value: "There is no spoon!", done: true}
4{value: undefined, done: true}

返回()方法迫使生成器对象完成并忽略任何其他输出关键字,这在非同步编程中尤其有用,当您需要使函数可以取消时,例如当用户想要执行不同的操作时中断网页请求,因为无法直接取消承诺。

如果发电机函数的身体有办法捕捉和处理错误,你可以使用投( )方法将错误扔进发电机,从而启动发电机,扔进错误,并终止发电机。

为了证明这一点,我们将把一个 try...catch放在发电机函数体内,如果找到一个错误,则会登录一个错误:

 1// Define a generator function with a try...catch
 2function* generatorFunction() {
 3  try {
 4    yield 'Neo'
 5    yield 'Morpheus'
 6  } catch (error) {
 7    console.log(error)
 8  }
 9}
10
11// Invoke the generator and throw an error
12const generator = generatorFunction()

现在,我们将运行next()方法,然后是throw():

1generator.next()
2generator.throw(new Error('Agent Smith!'))

这将产生以下产出:

1[secondary_label Output]
2{value: "Neo", done: false}
3Error: Agent Smith!
4{value: undefined, done: true}

使用投( ),我们将错误注入到发电机中,该错误被试...捕获捕获并登录到控制台。

发电机对象方法和状态

下表显示了可以用于生成器对象的方法列表:

MethodDescription
next()Returns the next value in a generator
return()Returns a value in a generator and finishes the generator
throw()Throws an error and finishes the generator

下表列出了一个生成器对象的可能状态:

StatusDescription
suspendedGenerator has halted execution but has not terminated
closedGenerator has terminated by either encountering an error, returning, or iterating through all values

代表团获利

除了正常的输出运算器外,发电机还可以使用输出表达式(LINK0)将进一步的值转移到另一个发电机上。当输出在发电机内遇到时,它会进入发电机内,并开始通过所有输出进行迭代,直到发电机关闭。

为了示范,我们可以创建两个发电机函数,其中一个将yield*在另一个操作:

 1// Generator function that will be delegated to
 2function* delegate() {
 3  yield 3
 4  yield 4
 5}
 6
 7// Outer generator function
 8function* begin() {
 9  yield 1
10  yield 2
11  yield* delegate()
12}

接下来,让我们通过 begin() 生成函数进行迭代:

1// Iterate through the outer generator
2const generator = begin()
3
4for (const value of generator) {
5  console.log(value)
6}

这将以它们生成的顺序提供以下值:

1[secondary_label Output]
21
32
43
54

外部发电机输出值12,然后转移到另一个发电机输出,返回34

yield*也可以转移到任何可迭代的对象,例如数组或地图。 yield delegation 可以帮助组织代码,因为在生成器中想要使用yield的任何函数也必须是生成器。

无限数据流

发电机的一个有用的方面是能够使用无限的数据流和集合,这可以通过在发电机函数中创建无限循环来证明。

在下列代码块中,我们定义这个发电机函数,然后启动发电机:

 1// Define a generator function that increments by one
 2function* incrementer() {
 3  let i = 0
 4
 5  while (true) {
 6    yield i++
 7  }
 8}
 9
10// Initiate the generator
11const counter = incrementer()

现在,通过使用 next() 的值迭代:

1// Iterate through the values
2counter.next()
3counter.next()
4counter.next()
5counter.next()

这将产生以下产出:

1[secondary_label Output]
2{value: 0, done: false}
3{value: 1, done: false}
4{value: 2, done: false}
5{value: 3, done: false}

函数返回无限循环中的连续值,而完成属性仍然是,确保它不会完成。

与发电机,你不必担心创建一个无限循环,因为你可以停止和恢复执行随心所欲. 然而,你仍然需要小心如何调用发电机. 如果你在无限数据流中使用 spread 或 `for...of,你仍然会同时重复一个无限循环,这将导致环境崩溃。

对于一个更复杂的无限数据流示例,我们可以创建一个Fibonacci生成函数。

 1// Create a fibonacci generator function
 2function* fibonacci() {
 3  let prev = 0
 4  let next = 1
 5
 6  yield prev
 7  yield next
 8
 9  // Add previous and next values and yield them forever
10  while (true) {
11    const newVal = next + prev
12
13    yield newVal
14
15    prev = next
16    next = newVal
17  }
18}

为了测试这一点,我们可以循环通过一个有限的数字,并将斐波纳奇序列打印到控制台上。

1// Print the first 10 values of fibonacci
2const fib = fibonacci()
3
4for (let i = 0; i < 10; i++) {
5  console.log(fib.next().value)
6}

这将给出如下:

 1[secondary_label Output]
 20
 31
 41
 52
 63
 75
 88
 913
1021
1134

能够与无限数据集一起工作是使发电机如此强大的一部分,这对于实施无限滚动在Web应用程序前端等实例来说很有用。

在发电机中传输值

在本文中,我们使用发电机作为迭代器,并在每个迭代中产生值. 除了产生值之外,发电机还可以消耗从 next() 的值. 在这种情况下, yield将包含一个值。

重要的是要注意,呼叫的第一个next()不会传递一个值,而只会启动发电器。 为了证明这一点,我们可以记录yield的值,并呼叫next()一些值。

 1function* generatorFunction() {
 2  console.log(yield)
 3  console.log(yield)
 4
 5  return 'The end'
 6}
 7
 8const generator = generatorFunction()
 9
10generator.next()
11generator.next(100)
12generator.next(200)

这将产生以下产出:

1[secondary_label Output]
2100
3200
4{value: "The end", done: true}

在以下示例中,我们将创建一个for循环并将每个值传入next()方法,但还将一个参数传递到初始函数中:

 1function* generatorFunction(value) {
 2  while (true) {
 3    value = yield value * 10
 4  }
 5}
 6
 7// Initiate a generator and seed it with an initial value
 8const generator = generatorFunction(0)
 9
10for (let i = 0; i < 5; i++) {
11  console.log(generator.next(i).value)
12}

我们将从next()中获取值,并在下一次迭代中产生一个新的值,这是前一个值十倍的值。

1[secondary_label Output]
20
310
420
530
640

处理发电机启动的另一种方法是将发电机包裹到一个函数中,在做其他任何事情之前,它总是会打电话给‘next()’。

async / 等待 与发电机

非同步函数是可在 ES6+ JavaScript 中使用的函数类型,通过使其看起来同步来使处理非同步数据更容易理解。发电机具有比非同步函数更广泛的功能,但能够复制类似的行为。

在本节中,我们将展示使用发电机复制 async/await的例子。

让我们构建一个非同步函数,该函数使用 Fetch APIJSONPlaceholder API获取数据(为测试提供示例 JSON数据),并将响应记录到控制台。

首先,定义一个名为getUsers的非同步函数,该函数从API中获取数据并返回一组对象,然后拨打getUsers:

1const getUsers = async function() {
2  const response = await fetch('https://jsonplaceholder.typicode.com/users')
3  const json = await response.json()
4
5  return json
6}
7
8// Call the getUsers function and log the response
9getUsers().then(response => console.log(response))

这将提供类似于以下的 JSON 数据:

1[secondary_label Output]
2[ {id: 1, name: "Leanne Graham" ...},
3  {id: 2, name: "Ervin Howell" ...},
4  {id: 3, name": "Clementine Bauch" ...}, 
5  {id: 4, name: "Patricia Lebsack"...},
6  {id: 5, name: "Chelsey Dietrich"...},
7  ...]

使用发电机,我们可以创建一些几乎相同的东西,而不是使用asyncWait的关键字,而不是使用我们创建的新函数和yield的值,而不是Wait的承诺。

在下面的代码块中,我们定义了一个名为getUsers的函数,该函数使用我们新的asyncAlt函数(我们将在稍后写下)来模仿async /await

1const getUsers = asyncAlt(function*() {
2  const response = yield fetch('https://jsonplaceholder.typicode.com/users')
3  const json = yield response.json()
4
5  return json
6})
7
8// Invoking the function
9getUsers().then(response => console.log(response))

正如我们所看到的,它看起来几乎与async / Wait 实现相同,但有一个生成器函数正在传递中,该函数会产生值。

现在我们可以创建一个类似于非同步函数的‘asyncAlt’函数。‘asyncAlt’具有作为参数的生成函数,这是我们的函数,使‘fetch’返回的承诺。

 1// Define a function named asyncAlt that takes a generator function as an argument
 2function asyncAlt(generatorFunction) {
 3  // Return a function
 4  return function() {
 5    // Create and assign the generator object
 6    const generator = generatorFunction()
 7
 8    // Define a function that accepts the next iteration of the generator
 9    function resolve(next) {
10      // If the generator is closed and there are no more values to yield,
11      // resolve the last value
12      if (next.done) {
13        return Promise.resolve(next.value)
14      }
15
16      // If there are still values to yield, they are promises and
17      // must be resolved.
18      return Promise.resolve(next.value).then(response => {
19        return resolve(generator.next(response))
20      })
21    }
22
23    // Begin resolving promises
24    return resolve(generator.next())
25  }
26}

这将提供与async/await版本相同的输出:

1[secondary_label Output]
2[ {id: 1, name: "Leanne Graham" ...},
3  {id: 2, name: "Ervin Howell" ...},
4  {id: 3, name": "Clementine Bauch" ...}, 
5  {id: 4, name: "Patricia Lebsack"...},
6  {id: 5, name: "Chelsey Dietrich"...},
7  ...]

请注意,此实现旨在展示如何将发电机用于代替async/await,并且不是一个生产准备的设计,它没有错误处理设置,也不具备将参数传输到产值的能力。

结论

发电机是可以停止和恢复执行的流程. 它们是JavaScript的强大而多功能的功能,虽然它们不经常使用。 在本教程中,我们了解了发电机函数和发电机对象,发电机可用的方法,‘yield’和‘yield*’操作员,以及与有限和无限数据集使用的发电机。

如果您想了解有关JavaScript语法的更多信息,请参阅我们的教程(理解,绑定,调用和应用在JavaScript中)和(理解JavaScript中的地图和设置对象)。

Published At
Categories with 技术
comments powered by Disqus