JavaScript 生成器简介
同步发电机
异步生成器
立即在http://jauyeung.net/subscribe/订阅我的电子邮件列表
在 Twitter 上关注我:https://twitter.com/AuMayeung
更多文章请访问https://medium.com/@hohanga
更多文章请访问http://thewebdev.info/
在 JavaScript 中,生成器是一种返回生成器对象的特殊函数。该生成器对象包含next
一个可迭代对象的值。它允许我们在for...of
循环中使用生成器函数来迭代对象集合。这意味着返回的生成器函数遵循可迭代协议。
同步发电机
任何遵循可迭代协议的东西都可以通过for...of
循环进行迭代。类似Array
或 的对象Map
都遵循此协议。生成器函数也遵循迭代器协议。这意味着它以标准方式生成值序列。它实现了next
返回至少具有 2 个属性的对象(done
和)的函数value
。done
属性是一个布尔值,当true
迭代器超出迭代序列的末尾时返回。如果它能够生成序列中的下一个项目,那么它就是false
。value
是迭代器返回的项目。如果done
是,true
则value
可以省略。该next
方法始终返回具有上述 2 个属性的对象。如果返回非对象值,TypeError
则会抛出 。
要编写生成器函数,我们使用以下代码:
function* strGen() {
yield 'a';
yield 'b';
yield 'c';
}
const g = strGen();
for (let letter of g){
console.log(letter)
}
function 关键字后的星号表示该函数是生成器函数。生成器函数只会返回生成器对象。使用生成器函数,函数next
会自动生成。生成器还具有一个return
函数用于返回给定值并结束生成器,以及一个throw
函数用于抛出错误并结束生成器,除非生成器内部捕获到错误。要从生成器返回下一个值,我们使用yield
关键字。每次yield
调用语句时,生成器都会暂停,直到next
再次请求该值。
当执行上述示例时,我们会记录“a”、“b”和“c”,因为生成器是在循环中运行的for...of
。每次运行时,都会yield
调用下一个语句,并返回语句列表中的下一个值yield
。
我们还可以编写一个生成器来生成无限的值。我们可以在生成器内部编写一个无限循环来不断返回新值。由于该yield
语句直到下一个值被请求时才会运行,因此我们可以保持无限循环运行而不会导致浏览器崩溃。例如,我们可以这样写:
function* genNum() {
let index = 0;
while(true){
yield index += 2;
}
}
const gen = genNum();
console.log(gen.next().value);
console.log(gen.next().value);
console.log(gen.next().value);
如您所见,我们可以使用循环来重复运行yield
。该yield
语句必须在代码的顶层运行。这意味着它们不能嵌套在另一个回调函数中。该next
函数会自动包含在生成器对象中,该生成器对象会被 yield 以从生成器中获取下一个值。
该return
方法在迭代器结束时调用。也就是说,当获取到最后一个值或该方法抛出错误时thrown
。如果我们不想迭代器结束,可以将yield
语句包装在子句中,try...finally
就像下面的代码一样:
function* genFn() {
try {
yield;
} finally {
yield 'Keep running';
}
}
在运行生成器时调用该throw
方法,错误将停止生成器,除非在生成器函数中捕获该错误。为了捕获、抛出并捕获错误,我们可以编写类似以下代码:
function* genFn() {
try {
console.log('Start');
yield; // (A)
} catch (error) {
console.log(`Caught: ${error}`);
}
}
const g = genFn();
g.next();
g.throw(new Error('Error'))
如您所见,如果我们运行上面的代码,我们可以看到在运行第一行时会打印“Start”,因为我们只是从生成器对象(也就是该g.next()
行)获取第一个值。然后g.throw(new Error('Error'))
运行该行会抛出一个错误,该错误会打印在catch
子句中。
使用生成器函数,我们还可以使用关键字在其中调用其他生成器函数yield*
。以下示例不起作用:
function* genFn() {
yield 'a'
}
function* genFnToCallgenFn() {
while (true) {
yield genFn();
}
}
const g = genFnToCallgenFn()
console.log(g.next())
console.log(g.next())
如您所见,如果我们运行上面的代码,value
logged 的属性就是生成器函数,这不是我们想要的。这是因为yield
关键字没有直接从其他生成器中检索值。这正是yield*
关键字有用的地方。如果我们yield genFn();
用替换yield* genFn();
,那么将检索由 生成的生成器返回的值genFn
。在这种情况下,它将继续获取字符串“a”。例如,如果我们改为运行以下代码:
function* genFn() {
yield 'a'
}
function* genFnToCallgenFn() {
while (true) {
yield* genFn();
}
}
const g = genFnToCallgenFn()
console.log(g.next())
console.log(g.next())
我们将看到,value
两个记录的对象中的属性都value
设置为“a”。
使用生成器,我们可以编写一个迭代方法,轻松地递归遍历一棵树。例如,我们可以编写以下内容:
class Tree{
constructor(value, left=null, center=null, right=null) {
this.value = value;
this.left = left;
this.center = center;
this.right = right;
}
*[Symbol.iterator]() {
yield this.value;
if (this.left) {
yield* this.left;
}
if (this.center) {
yield* this.center;
}
if (this.right) {
yield* this.right;
}
}
}
在上面的代码中,我们唯一的方法是一个生成器,它返回树中当前节点的左、中、右节点。请注意,我们使用yield*
关键字 而不是 ,yield
因为 JavaScript 类是生成器函数,而我们的类是一个生成器函数,因为我们有一个用 符号表示的特殊函数Symbol.iterator
,这意味着该类将创建一个生成器。
Symbol 是 ES2015 中的新特性。它是一个唯一且不可变的标识符。一旦创建,就无法复制。每次创建一个新的 Symbol 时,它都是唯一的。它主要用于对象中的唯一标识符。这也是 Symbol 的唯一用途。
它有一些自身的静态属性和方法,用于公开全局符号注册表。它就像一个内置对象,但是它没有构造函数,因此我们无法使用关键字new Symbol
构造一个 Symbol 对象new
。
Symbol.iterator
是一个特殊符号,表示该函数是一个迭代器。它内置于 JavaScript 标准库中。
如果我们有以下定义代码,那么我们就可以构建树形数据结构:
const tree = new Tree('a',
new Tree('b',
new Tree('c'),
new Tree('d'),
new Tree('e')
),
new Tree('f'),
new Tree('g',
new Tree('h'),
new Tree('i'),
new Tree('j')
)
);
然后我们运行:
for (const str of tree) {
console.log(str);
}
我们按照定义树的顺序记录了树的所有值。定义递归数据结构比不使用生成器函数要容易得多。
我们可以在一个生成器函数中混合使用yield
和yield*
。例如,我们可以这样写:
function* genFn() {
yield 'a'
}
function* genFnToCallgenFn() {
yield 'Start';
while (true) {
yield* genFn();
}
}
const g = genFnToCallgenFn()
console.log(g.next())
console.log(g.next())
console.log(g.next())
console.log(g.next())
如果我们运行上面的代码,我们会得到value
返回的第一个项目的属性为“Start” g.next()
。然后,记录的其他项目都具有“a”作为value
属性。
我们还可以使用return
语句返回迭代器中想要返回的最后一个值。它的作用yield
与生成器函数中的最后一条语句完全相同。例如,我们可以这样写:
function* genFn() {
yield 'a';
return 'result';
}
const g = genFn()
console.log(g.next())
console.log(g.next())
console.log(g.next())
console.log(g.next())
如果我们查看控制台日志,我们可以看到我们记录的前两行在属性中返回了“a” value
,并value
在前两console.log
行的属性中返回了“result”。然后剩下的一行将设置undefined
为value
。第一个console.log
设置done
为false
,而其余的done
设置为true
。这是因为该return
语句结束了生成器函数的执行。它下面的任何内容都像常规return
语句一样无法访问。
异步生成器
生成器也可用于异步代码。为了创建用于异步代码的生成器函数,我们可以创建一个对象,其中包含一个用特殊符号Symbol.asyncIterator
function 表示的方法。例如,我们可以编写以下代码来循环遍历一系列数字,每次迭代间隔 1 秒:
const rangeGen = (from = 1, to = 5) => {
return {
from,
to,
[Symbol.asyncIterator]() {
return {
currentNum: this.from,
lastNum: this.to,
async next() {
await new Promise(resolve => setTimeout(
resolve, 1000));
if (this.currentNum <= this.lastNum) {
return {
done: false,
value: this.currentNum++
};
} else {
return {
done: true
};
}
}
};
}
};
}
(async () => {
for await (let value of rangeGen()) {
console.log(value);
}
})()
请注意,promise 解析后的值在return
语句中。next
函数应始终返回一个 Promise。我们可以使用循环来迭代生成器生成的值,循环也适用于迭代异步代码。这非常有用,因为我们可以像循环同步代码一样循环执行异步代码,而这在异步生成器函数以及和语法for await...of
出现之前是无法实现的。我们返回一个具有和属性的对象,就像同步生成器一样。async
await
done
value
我们可以通过以下方式缩短上述代码:
async function* rangeGen(start = 1, end = 5) {
for (let i = start; i <= end; i++) {
await new Promise(resolve => setTimeout(resolve, 1000));
yield i;
}
}
(async () => {
for await (let value of rangeGen(1, 10)) {
console.log(value);
}
})()
注意,我们可以将yield
运算符与async
和 一起使用await
。 的末尾仍然会返回一个 Promise rangeGen
,但这是一种更简洁的方法。它的作用与之前的代码完全相同,但更简洁,也更易于阅读。
生成器函数对于创建可与循环一起使用的迭代器非常有用for...of
。该yield
语句将从您选择的任何来源获取迭代器返回的下一个值。这意味着我们可以将任何东西变成可迭代的对象。此外,我们可以使用它来迭代树结构,方法是定义一个类,该类具有由 Symbol 表示的方法Symbol.iterator
,该方法创建一个生成器函数,该函数使用关键字获取下一级的项目yield*
,该关键字直接从生成器函数获取一个项目。此外,我们还有return
语句来返回生成器函数中的最后一项。对于异步代码,我们有AsyncIterators
,我们可以通过使用async
、await
和来定义它yield
,就像我们上面所做的那样按顺序解决承诺。