了解JavaScript中的生成器
作为 Write for DOnations 计划的一部分,作者选择了 Open Internet/Free Speech Fund 来接受捐赠。
介绍
在 ECMAScript 2015 中,生成器被引入 JavaScript 语言。 generator 是一个可以暂停和恢复的进程,并且可以产生多个值。 JavaScript 中的生成器由 生成器函数 组成,它返回一个可迭代的 Generator 对象。
生成器可以维护状态,提供一种有效的方法来制作迭代器,并且能够处理无限的数据流,可用于在 Web 应用程序的前端实现无限滚动,对声波数据进行操作等等。 此外,当与 Promises 一起使用时,生成器可以模仿 async/await
功能,这使我们能够以更直接和可读的方式处理 异步代码 。 尽管 async/await
是处理常见的简单异步用例(例如从 API 获取数据)的一种更普遍的方式,但生成器具有更高级的功能,因此值得学习如何使用它们。
在本文中,我们将介绍如何创建生成器函数,如何迭代 Generator
对象,生成器内部 yield
和 return
之间的区别,以及其他方面与发电机一起工作。
生成器函数
生成器函数 是返回 Generator
对象的函数,由 function
关键字后跟星号 (*
) 定义,如图所示在下面的:
// Generator function declaration function* generatorFunction() {}
有时,您会在函数名称旁边看到星号,而不是函数关键字,例如 function *generatorFunction()
。 这同样适用,但 function*
是更广泛接受的语法。
生成器函数也可以在表达式中定义,如常规 functions:
// 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
对象。
生成器对象
传统上,JavaScript 中的函数运行完成,调用函数将在到达 return
关键字时返回一个值。 如果省略 return
关键字,函数将隐式返回 undefined
。
例如,在下面的代码中,我们声明了一个 sum()
函数,该函数返回一个值,该值是两个整数参数之和:
// A regular function that sums two values function sum(a, b) { return a + b }
调用该函数会返回一个值,该值是参数的总和:
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
将给出类似于以下的输出:
OutputgeneratorFunction {<suspended>} __proto__: Generator [[GeneratorLocation]]: VM272:1 [[GeneratorStatus]]: "suspended" [[GeneratorFunction]]: ƒ* generatorFunction() [[GeneratorReceiver]]: Window [[Scopes]]: Scopes[3]
函数返回的Generator
对象是一个iterator。 iterator 是具有 next()
方法可用的对象,用于迭代一系列值。 next()
方法返回具有 value
和 done
属性的对象。 value
表示返回值,done
表示迭代器是否遍历了它的所有值。
知道了这一点,让我们在 generator
上调用 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
。 再次调用 generator
将得到以下结果:
OutputgeneratorFunction {<closed>}
到目前为止,我们只演示了生成器函数如何成为获取函数的 return
值的更复杂的方法。 但是生成器函数也有区别于普通函数的独特特性。 在下一节中,我们将了解 yield
运算符,并了解生成器如何暂停和恢复执行。
yield
运算符
生成器为 JavaScript 引入了一个新关键字:yield。 yield
可以暂停生成器函数并返回 yield
之后的值,提供了一种轻量级的迭代值的方法。
在这个例子中,我们将使用不同的值暂停生成器函数 3 次,并在最后返回一个值。 然后我们将我们的 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
会在每个yield
之后设置为false
,表示生成器还没有完成。 一旦遇到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
遵循迭代协议,可以用迭代对于... :
// 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'
)。
注意:虽然这两种方法对于使用有限生成器都是有效的,但如果生成器正在处理无限数据流,则无法直接使用 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
。 如果我们在此之后立即在 Generator
对象上调用 return()
方法,我们现在将获得传递的值并将 done
设置为 true
。 对 next()
的任何额外调用都将给出默认完成的生成器响应,其值未定义。
为了证明这一点,在 generator
上运行以下三个方法:
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
关键字。 当您需要使函数可取消时,这在异步编程中特别有用,例如当用户想要执行不同的操作时中断 Web 请求,因为无法直接取消 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) }
这将按照它们生成的顺序给出以下值:
Output1 2 3 4
外部生成器产生值 1
和 2
,然后用 yield*
委托给另一个生成器,它返回 3
和 4
。
yield*
也可以委托给任何可迭代的对象,例如 Array 或 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()
遍历这些值:
// 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
,确保它不会完成。
使用生成器,您不必担心创建无限循环,因为您可以随意停止和恢复执行。 但是,您仍然必须小心调用生成器的方式。 如果您在无限数据流上使用 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) }
这将给出以下内容:
Output0 1 1 2 3 5 8 13 21 34
处理无限数据集的能力是生成器如此强大的原因之一。 这对于在 Web 应用程序的前端实现无限滚动等示例很有用。
在生成器中传递值
在整篇文章中,我们使用生成器作为迭代器,并且在每次迭代中都产生了值。 除了产生值之外,生成器还可以使用来自 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()
中检索值并为下一次迭代产生一个新值,即前一个值乘以 10。 这将给出以下内容:
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
/[X158X ]。
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
返回一个函数本身,并解析它找到的每一个承诺,直到最后一个:
// 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 语法的更多信息,请查看我们的 Understanding This, Bind, Call, and Apply in JavaScript 和 Understanding Map and Set Objects in JavaScript 教程。