JavaScript 中的生成器:如何使用它们
开场白
什么是发电机?
用例
结束语
开场白
大家好,程序员们👋 在本文中,我们将介绍 ES6 中引入的 Javascript 生成器的基础知识,并介绍一些实际的用例。
什么是发电机?
顾名思义,生成器是一个函数,它允许你通过退出并重新进入执行过程来生成一个或多个值,同时在多次调用之间保存其状态(上下文)。简而言之,生成器类似于普通函数,但它能够根据需要从上次终止的位置继续执行,只需保存先前的状态即可。以下流程图说明了普通函数和生成器函数之间的区别。
句法
正如您已经猜到的,普通函数和生成器之间存在一些语法差异:
// Normal Function
function normalFunction(params) {
  // your logic goes here
  return value;
}
/* --------------------------------- */
// Generator Function
function* generatorFunction(params) {
  // your logic
  yield value1;
  // your logic
  yield value2;
  /*
    .
    .
    .
  */
  // your logic
  yield valueN;
}
语法上第一个明显的区别是,生成器使用function*关键字 而不是 来声明。另外,请注意,在普通函数中function我们使用关键字,而在生成器函数中使用 关键字。生成器中的 关键字允许我们“返回”一个值、终止执行、保存当前词法作用域的状态(上下文),并等待下一次调用从上一个终止点恢复执行。returnyieldyield
注意:在普通函数中,该关键字只能执行return一次,执行后会返回一个值并完全终止函数。在生成器中,您可以yield根据需要多次使用该关键字,以便在连续调用时“返回”值。您也可以return在生成器内部使用该关键字,但这个问题留到以后再讨论。
调用
现在我们已经了解了这两个函数在语法上的差异,让我们看看如何调用生成器并 yield 它的返回值。首先,考虑以下代码片段,它演示了如何调用普通函数:
function normalFunction() {
  console.log('I have been invoked');
}
// invocation
normalFunction();
一般来说,你可以通过输入函数签名,后面跟着一对括号来调用一个普通函数()。上面的代码将输出:
I have been invoked
现在让我们尝试使用相同的过程来调用生成器。仔细检查以下代码:
function* generatorFunction() {
  console.log('I have been invoked');
  yield 'first value';
  console.log('resuming execution');
  yield 'second value';
}
// does this invoke the generator?
generatorFunction();
你对这样的程序有什么期望?从技术上讲,我们期望函数一直执行到遇到第一个 yield 关键字为止。然而,上一个程序的输出为空:
这是因为正常的调用语法实际上并不会执行生成器函数的主体。相反,它会创建一个Generator包含多个属性和方法的对象。为了证明这一点,我们可以尝试打印出来console.log(generatorFunction()),输出应该如下所示:
Object [Generator] {}
那么,问题是:我们实际上如何从生成器中产生我们的值?
好吧,有一些属于GeneratorObject 的重要方法我们可以利用。第一个也是最重要的方法是 called next(),顾名思义,它从定义的生成器中生成下一个值。现在让我们修改一下之前的代码,让它真正地生成我们需要的值:
function* generatorFunction() {
  console.log('I have been invoked');
  yield 'first value';
  console.log('resuming execution');
  yield 'second value';
}
// store the Generator Object in a variable
let foo = generatorFunction();
// execute until we yield the first value
console.log(foo.next());
// resume execution until we yield the second value
console.log(foo.next());
// execute until the function ends
console.log(foo.next());
上述代码的输出是:
I have been invoked
{ value: 'first value', done: false }
resuming execution
{ value: 'second value', done: false }
{ value: undefined, done: true }
让我们逐行检查输出。调用第一个foo.next()方法时,生成器开始执行,直到遇到第一个 yield 关键字才停止执行。这反映在输出的前两行中。请注意,它foo.next()返回的是 ,Object而不是实际的 yield 值。此对象应始终包含以下属性:
- 
  “值”:保存生成器当前产生的值。 
- 
  “done”:一个布尔标志,指示生成器执行是否已结束。 
让我们继续讨论第二个foo.next()调用。正如预期的那样,生成器从最后一个终止步骤继续执行,直到遇到第二个 yield 关键字,这反映在输出的第三行和第四行中。请注意,标志done仍然由 设置false,因为它尚未到达函数末尾。
在最后一次foo.next()调用时,函数在第二个 yield 关键字后恢复执行,并且发现没有可执行的内容,这表明我们已到达函数末尾。此时,没有其他值可供 yield,并且done标志位被设置为 ,true如输出的最后一行所示。
现在我们已经介绍了 Javascript 中生成器的基本概念,让我们来看看它的一些有用的用例。
用例
 用例 1:模拟range()Python 中的函数
根据 Python 文档,“该range类型表示不可变的数字序列,通常用于在 for 循环中循环特定次数。” range()Python 中的函数通常包含以下参数:
- 
  start(可选,默认值 = 0):序列中的第一个数字(含)。
- 
  end(必需):序列的最后一个数字,不包括。
- 
  step(可选,默认值 = 1):序列中任意两个给定数字之间的差值。
基本上,Python 中该函数的用法range()如下所示:
# Python code
for i range(3):
    print(i)
# output:
# 0
# 1
# 2
我们需要做的是使用生成器在 JavaScript 中模拟此功能。仔细检查以下代码:
/*
range function implemented in Javascript
*/
function* range({start = 0, end, step = 1}) {
  for (let i = start; i < end; i += step) yield i;
}
让我们一步一步来。首先,函数签名定义了一个生成器,它接受三个参数:start、end和step,其中start和分别step默认为0和1。进入函数主体,它包含一个基本的 for 循环,该循环从start包含到end不包含进行迭代。在循环作用域内,我们 yieldi序列中当前数字的值。
让我们看看它的实际效果。以下代码片段展示了实现range函数的不同示例:
// first example
for (let i of range({end: 4})) console.log(i);
/*
output:
0
1
2
3
*/
// second example
for (let i of range({start: 2, end: 4})) console.log(i);
/*
output:
2
3
*/
// third example
for (let i of range({start: 1, end: 8, step: 2})) console.log(i);
/*
output:
1
3
5
7
*/
用例 2:可视化冒泡排序算法
在这个用例中,我们将尝试输出对给定数组执行冒泡排序算法的逐步过程,以便于可视化。简而言之,冒泡排序的工作原理如下:给定一个长度为 的数组n,i作为当前迭代,将 反复传播max(array[0:n - i])到索引n - i,直到数组排序完成。默认实现如下所示:
/*
Bubble Sort implementation in javascript
*/
function bubbleSort(arr) {
  for (let i = arr.length - 1; i >= 0; i--) {
    for (let j = 0; j < i; j++) {
      // if the current value is larger than its adjacent
      // swap them together
      if (arr[j] > arr[j+1]) {
        [arr[j], arr[j+1]] = [arr[j+1], arr[j]];
      }
    }
  }
  return arr;
}
我们的任务是将算法中逐步进行的比较和交换可视化。这可以使用生成器轻松实现。我们只需在内循环每次迭代后 yield 当前数组即可。新函数如下所示:
/*
visualize Bubble Sort implementation in javascript
*/
function* visualizeBubbleSort(arr) {
  for (let i = arr.length - 1; i >= 0; i--) {
    for (let j = 0; j < i; j++) {
      if (arr[j] > arr[j + 1]) {
        [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
      }
      yield arr;
    }
  }
}
这将在内循环中每次迭代时生成一个数组,并显示数组的当前状态。请考虑以下示例:
let inputArray = [40, 30, 2, 20];
let currentStep = 1;
for (let val of visualizeBubbleSort(inputArray)) {
  console.log(`step #${currentStep}: [${val}]`);
  currentStep++;
}
上述程序的输出将是:
step #1: [30,40,2,20]
step #2: [30,2,40,20]
step #3: [30,2,20,40]
step #4: [2,30,20,40]
step #5: [2,20,30,40]
step #6: [2,20,30,40]
通过实现的生成器,我们可以清楚地看到整个算法中发生的情况:
- 
  步骤 1 -> 40交换30
- 
  步骤 2 -> 40交换2
- 
  步骤 3 -> 40交换20
- 
  步骤 4 -> 30交换2
- 
  步骤 5 -> 30交换20
- 
  步骤 6 -> 不交换任何东西,数组是排序的 
注意:此技术可以轻松用于可视化任何给定的算法。有时它会非常有用。
用例 3:按需生成不同的随机数
在这个用例中,我们将尝试使用生成器生成一系列不同的随机数。首先,我们对输入和输出施加一些约束,如下所示:
- 
  该函数应该只生成正整数。 
- 
  该函数应该采用一个参数 limit,该参数决定生成的最大整数数量以及可能生成的最大整数。
- 
  该函数应该有一种方法来存储可供选择的有效整数池。 
仔细遵循前面的约束,我们可以轻松地使用生成器实现此功能:
/*
distinctRandom implementation in js 
*/
function* distinctRandom({limit = 10}) {
  // we create an array that contains all numbers in range [0:limit)
  // this is our initial pool of numbers to choose from
  const availableValues = [...new Array(limit)].map((val, index) => index);
  // we repeatedly loop until the available pool of numbers is empty
  while (availableValues.length !== 0) {
    // generate a random index in range [0: availableValues.length)
    // then, yield the number that is present at the chosen index
    // Finally, remove the picked item from the pool of available numbers
    const currentRandom = Math.floor(Math.random() * availableValues.length);
    yield availableValues[currentRandom];
    availableValues.splice(currentRandom, 1);
  }
}
简而言之,之前的生成器会尝试维护一个可用整数池以供选择。在每次迭代中,我们会从该池中随机选择一个数字,然后将其生成并移除。理论上,生成的整数的最大数量应该等于,limit并且所有生成的整数必须不同。我们可以通过耗尽已实现的生成器直到执行结束来轻松证明这一点:
// we set the limit to 8
for (const val of distinctRandom({limit: 8})) {
  console.log(val);
}
/*
sample output:
3
7
5
2
4
0
1
6
*/
结束语
生成器是 ES6 的一大亮点,它为多种问题和用例提供了解决方案。当然,你可以在任何地方使用它们,但我建议在最终确定使用生成器之前,先研究一下当前问题的替代方案,因为它们可能会增加代码的复杂性,有时也难以调试。不过,祝你编码愉快🎉
文章来源:https://dev.to/karimelghamry/generators-in-javascript-how-to-use-them-372d 后端开发教程 - Java、Spring Boot 实战 - msg200.com
            后端开发教程 - Java、Spring Boot 实战 - msg200.com
          