JavaScript 中的生成器:如何使用它们?开篇:什么是生成器?用例:结束语

2025-05-27

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;
}

Enter fullscreen mode Exit fullscreen mode

语法上第一个明显的区别是,生成器使用function*关键字 而不是 来声明。另外,请注意,在普通函数中function我们使用关键字,而在生成器函数中使用 关键字。生成器中的 关键字允许我们“返回”一个值、终止执行、保存当前词法作用域的状态(上下文),并等待下一次调用从上一个终止点恢复执行。returnyieldyield

注意:在普通函数中,该关键字只能执行return一次,执行后会返回一个值并完全终止函数。在生成器中,您可以yield根据需要多次使用该关键字,以便在连续调用时“返回”值。您也可以return在生成器内部使用该关键字,但这个问题留到以后再讨论。

调用

现在我们已经了解了这两个函数在语法上的差异,让我们看看如何调用生成器并 yield 它的返回值。首先,考虑以下代码片段,它演示了如何调用普通函数:

function normalFunction() {
  console.log('I have been invoked');
}

// invocation
normalFunction();
Enter fullscreen mode Exit fullscreen mode

一般来说,你可以通过输入函数签名,后面跟着一对括号来调用一个普通函数()。上面的代码将输出:

I have been invoked
Enter fullscreen mode Exit fullscreen mode

现在让我们尝试使用相同的过程来调用生成器。仔细检查以下代码:

function* generatorFunction() {
  console.log('I have been invoked');
  yield 'first value';

  console.log('resuming execution');
  yield 'second value';
}

// does this invoke the generator?
generatorFunction();

Enter fullscreen mode Exit fullscreen mode

你对这样的程序有什么期望?从技术上讲,我们期望函数一直执行到遇到第一个 yield 关键字为止。然而,上一个程序的输出为空:


Enter fullscreen mode Exit fullscreen mode

这是因为正常的调用语法实际上并不会执行生成器函数的主体。相反,它会创建一个Generator包含多个属性和方法的对象。为了证明这一点,我们可以尝试打印出来console.log(generatorFunction()),输出应该如下所示:

Object [Generator] {}
Enter fullscreen mode Exit fullscreen mode

那么,问题是:我们实际上如何从生成器中产生我们的值?

好吧,有一些属于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());

Enter fullscreen mode Exit fullscreen mode

上述代码的输出是:

I have been invoked
{ value: 'first value', done: false }
resuming execution
{ value: 'second value', done: false }
{ value: undefined, done: true }

Enter fullscreen mode Exit fullscreen mode

让我们逐行检查输出。调用第一个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

Enter fullscreen mode Exit fullscreen mode

我们需要做的是使用生成器在 JavaScript 中模拟此功能。仔细检查以下代码:

/*
range function implemented in Javascript
*/
function* range({start = 0, end, step = 1}) {
  for (let i = start; i < end; i += step) yield i;
}
Enter fullscreen mode Exit fullscreen mode

让我们一步一步来。首先,函数签名定义了一个生成器,它接受三个参数:startendstep,其中start和分别step默认为01。进入函数主体,它包含一个基本的 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
*/

Enter fullscreen mode Exit fullscreen mode

用例 2:可视化冒泡排序算法

在这个用例中,我们将尝试输出对给定数组执行冒泡排序算法的逐步过程,以便于可视化。简而言之,冒泡排序的工作原理如下:给定一个长度为 的数组ni作为当前迭代,将 反复传播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;
}
Enter fullscreen mode Exit fullscreen mode

我们的任务是将算法中逐步进行的比较和交换可视化。这可以使用生成器轻松实现。我们只需在内循环每次迭代后 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;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

这将在内循环中每次迭代时生成一个数组,并显示数组的当前状态。请考虑以下示例:

let inputArray = [40, 30, 2, 20];
let currentStep = 1;
for (let val of visualizeBubbleSort(inputArray)) {
  console.log(`step #${currentStep}: [${val}]`);
  currentStep++;
}
Enter fullscreen mode Exit fullscreen mode

上述程序的输出将是:

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]
Enter fullscreen mode Exit fullscreen mode

通过实现的生成器,我们可以清楚地看到整个算法中发生的情况:

  • 步骤 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);
  }
}
Enter fullscreen mode Exit fullscreen mode

简而言之,之前的生成器会尝试维护一个可用整数池以供选择。在每次迭代中,我们会从该池中随机选择一个数字,然后将其生成并移除。理论上,生成的整数的最大数量应该等于,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
*/
Enter fullscreen mode Exit fullscreen mode

结束语

生成器是 ES6 的一大亮点,它为多种问题和用例提供了解决方案。当然,你可以在任何地方使用它们,但我建议在最终确定使用生成器之前,先研究一下当前问题的替代方案,因为它们可能会增加代码的复杂性,有时也难以调试。不过,祝你编码愉快🎉

文章来源:https://dev.to/karimelghamry/generators-in-javascript-how-to-use-them-372d
PREV
Maxun:开源无代码 Web 数据提取平台⚡️
NEXT
Dockerize系列介绍