Generator 组合
Generator 组合(composition)是 generator 的一个特殊功能,它允许透明地(transparently)将 generator 彼此“嵌入(embed)”到一起。
例如,我们有一个生成数字序列的函数:
function* generateSequence(start, end) {
for (let i = start; i <= end; i++) yield i;
}
现在,我们想重用它来生成一个更复杂的序列:
- 首先是数字
0..9
(字符代码为 48…57), - 接下来是大写字母
A..Z
(字符代码为 65…90) - 接下来是小写字母
a...z
(字符代码为 97…122)
我们可以对这个序列进行应用,例如,我们可以从这个序列中选择字符来创建密码(也可以添加语法字符),但让我们先生成它。
在常规函数中,要合并其他多个函数的结果,我们需要调用它们,存储它们的结果,最后再将它们合并到一起。
对于 generator 而言,我们可以使用 yield*
这个特殊的语法来将一个 generator “嵌入”(组合)到另一个 generator 中:
组合的 generator 的例子:
function* generateSequence(start, end) {
for (let i = start; i <= end; i++) yield i;
}
function* generatePasswordCodes() {
// 0..9
yield* generateSequence(48, 57);
// A..Z
yield* generateSequence(65, 90);
// a..z
yield* generateSequence(97, 122);
}
let str = '';
for(let code of generatePasswordCodes()) {
str += String.fromCharCode(code);
}
alert(str); // 0..9A..Za..z
yield*
指令将执行 委托 给另一个 generator。这个术语意味着 yield* gen
在 generator gen
上进行迭代,并将其产出(yield)的值透明地(transparently)转发到外部。就好像这些值就是由外部的 generator yield 的一样。
执行结果与我们内联嵌套 generator 中的代码获得的结果相同:
function* generateSequence(start, end) {
for (let i = start; i <= end; i++) yield i;
}
function* generateAlphaNum() {
// yield* generateSequence(48, 57);
for (let i = 48; i <= 57; i++) yield i;
// yield* generateSequence(65, 90);
for (let i = 65; i <= 90; i++) yield i;
// yield* generateSequence(97, 122);
for (let i = 97; i <= 122; i++) yield i;
}
let str = '';
for(let code of generateAlphaNum()) {
str += String.fromCharCode(code);
}
alert(str); // 0..9A..Za..z
Generator 组合(composition)是将一个 generator 流插入到另一个 generator 流的自然的方式。它不需要使用额外的内存来存储中间结果。
“yield” 是一条双向路
目前看来,generator 和可迭代对象类似,都具有用来生成值的特殊语法。但实际上,generator 更加强大且灵活。
这是因为 yield
是一条双向路(two-way street):它不仅可以向外返回结果,而且还可以将外部的值传递到 generator 内。
调用 generator.next(arg)
,我们就能将参数 arg
传递到 generator 内部。这个 arg
参数会变成 yield
的结果。
我们来看一个例子:
function* gen() {
// 向外部代码传递一个问题并等待答案
let result = yield "2 + 2 = ?"; // (*)
alert(result);
}
let generator = gen();
let question = generator.next().value; // <-- yield 返回的 value
generator.next(4); // --> 将结果传递到 generator 中
- 第一次调用
generator.next()
应该是不带参数的(如果带参数,那么该参数会被忽略)。它开始执行并返回第一个yield "2 + 2 = ?"
的结果。此时,generator 执行暂停,而停留在(*)
行上。 - 然后,正如上面图片中显示的那样,
yield
的结果进入调用代码中的question
变量。 - 在
generator.next(4)
,generator 恢复执行,并获得了4
作为结果:let result = 4
。
请注意,外部代码不必立即调用 next(4)
。外部代码可能需要一些时间。这没问题:generator 将等待它。
例如:
// 一段时间后恢复 generator
setTimeout(() => generator.next(4), 1000);
我们可以看到,与常规函数不同,generator 和调用 generator 的代码可以通过在 next/yield
中传递值来交换结果。
为了讲得更浅显易懂,我们来看另一个例子,其中包含了许多调用:
function* gen() {
let ask1 = yield "2 + 2 = ?";
alert(ask1); // 4
let ask2 = yield "3 * 3 = ?"
alert(ask2); // 9
}
let generator = gen();
alert( generator.next().value ); // "2 + 2 = ?"
alert( generator.next(4).value ); // "3 * 3 = ?"
alert( generator.next(9).done ); // true
执行图:
- 第一个
.next()
启动了 generator 的执行……执行到达第一个yield
。 - 结果被返回到外部代码中。
- 第二个
.next(4)
将4
作为第一个yield
的结果传递回 generator 并恢复 generator 的执行。 - ……执行到达第二个
yield
,它变成了 generator 调用的结果。 - 第三个
next(9)
将9
作为第二个yield
的结果传入 generator 并恢复 generator 的执行,执行现在到达了函数的最底部,所以返回done: true
。
这个过程就像“乒乓球”游戏。每个 next(value)
(除了第一个)传递一个值到 generator 中,该值变成了当前 yield
的结果,然后获取下一个 yield
的结果。
generator.throw
正如我们在上面的例子中观察到的那样,外部代码可能会将一个值传递到 generator,作为 yield
的结果。
……但是它也可以在那里发起(抛出)一个 error。这很自然,因为 error 本身也是一种结果。
要向 yield
传递一个 error,我们应该调用 generator.throw(err)
。在这种情况下,err
将被抛到对应的 yield
所在的那一行。
例如,"2 + 2?"
的 yield 导致了一个 error:
function* gen() {
try {
let result = yield "2 + 2 = ?"; // (1)
alert("The execution does not reach here, because the exception is thrown above");
} catch(e) {
alert(e); // 显示这个 error
}
}
let generator = gen();
let question = generator.next().value;
generator.throw(new Error("The answer is not found in my database")); // (2)
在第二行引入到 generator 的 error 导致了在第一行中的 yield
出现了一个异常。在上面这个例子中,try..catch
捕获并显示了这个 error。
如果我们没有捕获它,那么就会像其他的异常一样,它将从 generator “掉出”到调用代码中。
调用代码的当前行是 generator.throw
所在的那一行,标记为 (2)
。所以我们可以在这里捕获它,就像这样:
function* generate() {
let result = yield "2 + 2 = ?"; // 这行出现 error
}
let generator = generate();
let question = generator.next().value;
try {
generator.throw(new Error("The answer is not found in my database"));
} catch(e) {
alert(e); // 显示这个 error
}
如果我们没有在那里捕获这个 error,那么,通常,它会掉入外部调用代码(如果有),如果在外部也没有被捕获,则会杀死脚本。
总结
- Generator 是通过 generator 函数
function* f(…) {…}
创建的。 - 在 generator(仅在)内部,存在
yield
操作。 - 外部代码和 generator 可能会通过
next/yield
调用交换结果。
在现代 JavaScript 中,generator 很少被使用。但有时它们会派上用场,因为函数在执行过程中与调用代码交换数据的能力是非常独特的。而且,当然,它们非常适合创建可迭代对象。