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
我们使用关键字,而在生成器函数中使用 关键字。生成器中的 关键字允许我们“返回”一个值、终止执行、保存当前词法作用域的状态(上下文),并等待下一次调用从上一个终止点恢复执行。return
yield
yield
注意:在普通函数中,该关键字只能执行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] {}
那么,问题是:我们实际上如何从生成器中产生我们的值?
好吧,有一些属于Generator
Object 的重要方法我们可以利用。第一个也是最重要的方法是 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