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
函数自动包含在生成器对象中,生成该对象可从生成器获取下一个值。
的 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'))
如您所见,如果我们运行上面的代码,则可以看到在运行第一行时记录了“开始”,因为我们只是从生成器对象获取第一个值,即 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
记录的属性是生成器函数,这不是我们想要的。这是因为 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
符号,表示该类将创建一个生成器。
符号是ES2015的新增功能。它是唯一且不变的标识符。创建后,将无法复制。每次创建新符号时,它都是唯一的。它主要用于对象中的唯一标识符。这是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
返回的第一项的属性 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())
如果我们查看控制台日志,可以看到我们记录的前2行在 value
属性,并在 value
前2个属性 console.log
线。然后剩下的一个 undefined
作为 value
。首先 console.log
已 done
调成 false
,而其余的 done
调成 true
。这是因为 return
语句结束了生成器函数的执行。低于它的任何东西都无法像普通的一样 return
声明。
异步发电机
生成器也可以用于异步代码。要为异步代码生成一个生成器函数,我们可以创建一个带有特殊符号表示的方法的对象 Symbol.asyncIterator
功能。例如,我们可以编写以下代码来遍历一系列数字,将每次迭代间隔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
。承诺仍会在末尾返回 rangeGen
,但这是一种更短的方法。它的功能与之前的代码完全相同,但是它更短,更容易阅读。
生成器函数对于创建可与 for...of
环。的 yield
语句将获得迭代器从您选择的任何源返回的下一个值。这意味着我们可以将任何东西变成可迭代的对象。另外,我们可以使用Symbol表示的方法定义一个类,从而使用它遍历树结构 Symbol.iterator
,这将创建一个生成器函数,该函数将使用 yield*
关键字,它直接从生成器函数中获取一项。另外,我们有 return
语句以返回生成器函数中的最后一项。对于异步代码,我们有 AsyncIterators
,我们可以使用定义 async
, await
和 yield
就像我们上面所做的那样以顺序解决承诺。