Menu Close

什么是JavaScript 的生成器,yield 操作符, 如何在生成器中传递值?

JavaScript生成器(Generator)是一种特殊类型的函数,它可以在需要时暂停执行,然后在稍后恢复。生成器是迭代器和函数的结合。

Javascript function and generator
Javascript function and generator

基本语法

生成器的语法类似函数,但是多了一个星号(*)。生成器函数是一个返回生成器对象的函数,它由关键字 function 后跟一个星号 * 定义:

generatorFunction  函数(生成器)名

arguments — 函数(生成器)的参数

statements — 函数(生成器)体

偶尔,你会看到星号紧跟在函数名后面,而不是在 function 关键字后面,比如 function *generatorFunction()。这样写是有效的,但 function* 是更为广泛接受的语法。

生成器函数也可以像常规函数一样在表达式中定义:

生成器甚至可以是对象或类的方法:

返回

现在你知道如何声明生成器函数了,让我们来看看它们返回的可迭代的生成器对象。

生成器对象

传统上,JavaScript中的函数会执行完毕,调用函数将在到达return关键字时返回一个值。如果省略了return关键字,函数将隐式返回undefined

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

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

然而,生成器函数不会立即返回一个值,而是返回一个可迭代的生成器对象。在下面的例子中,我们声明了一个函数并给它一个单一的返回值,就像标准函数一样:

当我们调用生成器函数时,它将返回生成器对象,我们可以将其赋值给一个变量:

如果这是一个普通函数,我们期望生成器会给我们函数中返回的字符串。然而,实际上我们得到的是一个处于暂停状态的对象。因此,调用生成器将会得到类似下面的输出:

Output
generatorFunction {<suspended>}
  __proto__: Generator
  [[GeneratorLocation]]: VM272:1
  [[GeneratorStatus]]: "suspended"
  [[GeneratorFunction]]: ƒ* generatorFunction()
  [[GeneratorReceiver]]: Window
  [[Scopes]]: Scopes[3]

由函数返回的生成器对象是一个迭代器。迭代器是一个具有可用的next()方法的对象,用于迭代一系列值。next()方法返回一个带有valuedone属性的对象。value表示返回的值,而done指示迭代器是否已经遍历完所有的值。

了解了这一点,让我们调用我们的生成器的next()方法,获取迭代器的当前值和状态:

这将得到以下输出:

 Output
{value: "Hello, Generator!", done: true}

调用next()返回的值是Hello, Generator!,而done的状态为true,因为这个值来自于一个结束迭代器的return语句。由于迭代器已经完成,生成器函数的状态将从暂停变为关闭。再次调用生成器将得到以下结果:

Output
generatorFunction {<closed>}

&nbsp;

adgfdfg

返回

生成器函数在执行时返回一个特殊的对象,称为生成器对象。这个生成器对象符合迭代器协议,它有一个next方法,可以用来迭代生成器函数的每个步骤。

当调用生成器函数时,它并不立即执行函数体,而是返回一个生成器对象。每次调用生成器对象的next方法,生成器函数会执行直到遇到yield语句,然后将yield后面的值作为next方法的返回值。当生成器函数执行结束时,next方法返回的对象中的done属性为true,表示生成器已经完成执行。

以下是一个简单的例子:

在这个例子中,myGenerator函数返回的生成器对象可以通过调用next方法来迭代生成的值。当生成器函数执行到最后一条yield语句时,后续的调用将返回{ value: undefined, done: true },表示生成器函数执行结束。

到目前为止,我们只演示了生成器函数如何成为获取函数返回值的一种更复杂的方式。但是生成器函数还具有使它们与普通函数有所区别的独特特性。

yield 操作符

生成器引入了 JavaScript 中的一个新关键字:yieldyield 可以暂停生成器函数并返回紧随其后的值,为迭代值提供了一种轻量级的方式。

yield 操作符是在 JavaScript 的生成器函数中使用的关键字,用于控制生成器的执行流。yield 的主要作用是在生成器函数内部产生一个值,并将控制权返回给调用方。生成器函数可以通过多次使用 yield 操作符来多次产生值,每次产生一个值后会暂停执行,等待下一次调用。

生成器函数通过 function* 定义,而 yield 操作符通常与 yield 语句一起使用。当生成器的 next() 方法被调用时,生成器函数执行到 yield 语句时暂停,将 yield 后面的值作为 next() 的返回值,同时保存函数的当前状态,以便在下一次调用时从暂停的地方继续执行。

在下面例子中,我们将使用不同的值三次暂停生成器函数,并在最后返回一个值。然后,我们将生成器对象分配给变量 generator

现在,当我们在生成器函数上调用 next() 时,它会在每次遇到 yield 时暂停。每次遇到 yield 后,done 将设置为 false,表示生成器尚未完成。一旦遇到 return,或者在函数中不再遇到 yielddone 将切换为 true,生成器就完成了。

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

这将按顺序生成以下四行输出:

Output
{value: "Neo", done: false}
{value: "Morpheus", done: false}
{value: "Trinity", done: false}
{value: "The Oracle", done: true}

请注意,生成器不需要 return。如果省略了 return,最后一次迭代将返回 {value: undefined, done: true},并且在生成器完成后,对 next() 的任何后续调用也将返回相同的值。

迭代生成器

使用 next() 方法,我们手动迭代了生成器对象,获取了完整对象的所有 valuedone 属性。然而,就像数组(Array)、映射(Map)和集合(Set)一样,生成器遵循迭代协议,可以使用 for...of 进行迭代

结果

Output
Neo
Morpheus
Trinity

扩展运算符也可以用于将生成器的值分配给一个数组

 

这将产生以下数组:

  Output
(3) ["Neo", "Morpheus", "Trinity"]

扩展运算符for...of 循环都不会将 return 语句的值包含在生成器的值中(在这个例子中,它应该是 ‘The Oracle’)。

注意:虽然这两种方法对于处理有限生成器非常有效,但如果生成器处理无限数据流,直接使用扩展运算符for...of 将导致无限循环。

关闭生成器

正如我们所看到的,通过迭代生成器的所有值,可以将其 done 属性设置为 true,并将其状态设置为已关闭。有两种额外的方法可以立即取消生成器:使用 return() 方法和使用 throw() 方法。

使用 return(),可以在任何地方终止生成器,就好像在函数体中有一个 return 语句一样。你可以向 return() 传递参数,也可以留空以获取 undefined 值。

为了演示 return(),我们将创建一个生成器,其中包含一些 yield 值,但在函数定义中没有 return

第一个 next() 将给我们 ‘Neo’,并将 done 设置为 false。如果我们在这之后立即调用 return() 方法,我们将得到传递的值,并且 done 被设置为 true。对 next() 的任何其他调用将给出默认的生成器完成响应,其值为 undefined

为了演示这一点,请在生成器上运行以下三个方法:

这将得到以下三个结果:

Output
{value: "Neo", done: false}
{value: "There is no spoon!", done: true}
{value: undefined, done: true}

return() 方法强制生成器对象完成,并忽略任何其他 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():

这将产生以下输出:

Output
{value: "Neo", done: false}
Error: Agent Smith!
{value: undefined, done: true}

使用 throw(),我们向生成器注入了一个错误,该错误被 try...catch 捕获并记录到控制台。

生成器对象的方法和状态

以下表格显示了可以在生成器对象上使用的方法列表:

以下是与生成器对象相关的方法和状态列表:

生成器对象方法:

  1. next(): 将生成器推进到下一个 yield 语句,并返回一个带有属性 { value, done } 的对象。value 属性包含产生的值,而 done 是一个布尔值,指示生成器是否已完成。
  2. return(value): 关闭生成器,可选择将一个值传递给生成器作为最终结果。对 next() 的后续调用将返回 { value: undefined, done: true }
  3. throw(error): 在生成器当前暂停的地方抛出错误。如果生成器有一个 try...catch 块,它将捕获错误,否则将被传播到调用代码。

生成器对象状态:

  1. 暂停(Suspended): 生成器的初始状态。生成器没有在主动执行,但准备好开始。
  2. 执行中(Executing): 生成器当前正在执行,并且尚未遇到 yield 语句或尚未完成。
  3. 已完成(Completed): 生成器已完成执行。通过 next() 返回的对象的 done 属性将为 true,对 next() 的后续调用将返回 { value: undefined, done: true }

这些方法和状态共同允许你控制生成器的执行流,并在其进展过程中检索值。

yield 委托(Delegation)

除了常规的 yield 操作符之外,生成器还可以使用 yield* 表达式将进一步的值委托给另一个生成器。当在生成器内部遇到 yield* 时,它将进入被委托的生成器,并开始迭代所有的 yield,直到该生成器关闭。这可以用于将不同的生成器函数分开,以语义方式组织你的代码,同时仍然可以按正确的顺序迭代它们的所有 yield

为了演示,我们可以创建两个生成器函数,其中一个将对另一个进行 yield* 操作:

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

这将按照它们生成的顺序给出以下值:

Output
1
2
3
4

外部生成器产生了值 1 和 2,然后通过 yield* 委托给了另一个生成器,它返回了值 3 和 4。

yield* 也可以委托给任何可迭代的对象,比如数组或映射。通过 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,确保它不会结束。

使用生成器,你不必担心创建无限循环,因为你可以随时中止和恢复执行。然而,你在调用生成器时仍然要小心。如果你在无限数据流上使用扩展运算符或 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)
}
  

这将产生以下输出:

 Output
0
1
1
2
3
5
8
13
21
34                      

能够处理无限数据集是使生成器如此强大的一部分。这在实现前端Web应用程序上的无限滚动等示例中非常有用。

在生成器中传递值

在整个文章中,我们将生成器用作迭代器,并在每次迭代中产生值。除了生成值之外,生成器还可以从 next() 中消耗值。在这种情况下,yield 将包含一个值。

需要注意的是,第一次调用 next() 将不会传递值,而仅仅是启动生成器。为了演示这一点,我们可以记录 yield 的值,并使用一些值调用 next() 几次。

这将产生以下输出:

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()。

 

 

wer

 

 

除教程外,本网站大部分文章来自互联网,如果有内容冒犯到你,请联系我们删除!
Posted in JavaScript教程