生成器
生成器函数
可以使用生成器函数返回一系列值:
function* generator() {
yield 1;
yield 2;
return 3;
}
var gen = generator();
console.log(gen);
// Object [Generator] {}
如示例所示,使用function*定义的生成器函数,会在调用后返回一个生成器对象(Generator),生成器对象有一个next方法,可以用于遍历生成器产生的值:
let next;
do {
next = gen.next();
console.log(next.value);
} while (!next.done);
// 1
// 2
// 3
生成器对象符合可迭代对象的定义,因此可以使用for...of进行遍历:
for (let n of generator()) {
console.log(n);
}
// 1
// 2
不过需要注意的是,这种情况下不会遍历生成器函数return返回的值,因此需要修改为:
function* generator2() {
yield 1;
yield 2;
yield 3;
}
for (let n of generator2()) {
console.log(n);
}
// 1
// 2
// 3
生成器对象同样可以使用展开语法:
let numbers = [0, generator2()];
console.log(numbers);
// [ 0, 1, 2, 3 ]
可迭代对象
在前面介绍可迭代对象时的一个例子:
let range = {
from: 0,
to: 5,
[Symbol.iterator]() {
return {
current: this.from,
last: this.to,
next() {
if (this.current <= this.last) {
return { done: false, value: this.current++ };
} else {
return { done: true };
}
},
};
},
}
for (let n of range) {
console.log(n);
}
实际上属性Symbol.iterator返回的就是一个生成器对象,因此它可以用一个生成器函数代替:
let range2 = {
from: 0,
to: 5,
[Symbol.iterator]: function* () {
for (let value = this.from; value <= this.to; value++) {
yield value;
}
},
}
for (let n of range2) {
console.log(n);
}
或者:
*[Symbol.iterator]() {
for (let value = this.from; value <= this.to; value++) {
yield value;
}
},
生成器组合
可以通过在生成器函数中“嵌入”其他生成器函数的方式组合生成器:
function* numberGenerator(start, end){
for(let i = start; i <= end; i++){
yield i;
}
}
function* charGenerator(){
yield* numberGenerator('0'.charCodeAt(), '9'.charCodeAt());
yield* numberGenerator('A'.charCodeAt(), 'Z'.charCodeAt());
yield* numberGenerator('a'.charCodeAt(), 'z'.charCodeAt());
}
let chars = [charGenerator()].map(char => String.fromCharCode(char));
console.log(chars);
// [
// '0', '1', '2', '3', '4', '5', '6', '7', '8',
// '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
// 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q',
// 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
// 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i',
// 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r',
// 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'
// ]
这里用一个特殊的yield*引入其它生成器函数,并将产生的值“抛出”。
上面的例子等价于:
function* charGenerator2(){
for(let i of numberGenerator('0'.charCodeAt(), '9'.charCodeAt())){
yield i;
}
for(let i of numberGenerator('A'.charCodeAt(), 'Z'.charCodeAt())){
yield i;
}
for(let i of numberGenerator('a'.charCodeAt(), 'z'.charCodeAt())){
yield i;
}
}
传递数据
实际上生成器中的yield不仅可以向外产生数据,还可以从外部接收数据:
function* sayGenerator(){
let msg = yield 'Hi, I\'m Jeck';
console.log(msg);
msg = yield 'Hi, LiLei, How are you?';
console.log(msg);
yield 'I\'m fine too';
}
let generator3 = sayGenerator();
console.log(generator3.next().value);
console.log(generator3.next('Hi, Jeck, I\'m LiLei').value);
console.log(generator3.next('I\'m fine, thank you, and you?').value);
// Hi, I'm Jeck
// Hi, Jeck, I'm LiLei
// Hi, LiLei, How are you?
// I'm fine, thank you, and you?
// I'm fine too
这和 Python 协程中的
yield用法类似。
首先,客户端程序调用生成器对象的.next()方法时,生成器函数会执行到yield,并将产生的内容('Hi, I\'m Jeck')包装成{value:...,done:...}返回,此时生成器函数会停在let msg = yield,并等待从外部传入一个值。
客户端程序第二次调用.next方法,并传入一个参数'Hi, Jeck, I\'m LiLei',生成器函数从停止的位置继续执行,接收这个参数并打印到控制台,然后停止在第二个 yield 位置,并将 'Hi, LiLei, How are you?' 值返回。
以此类推。
异常处理
如果生成器对象在等待外部传入参数,此时外部程序可能产生异常:
function* generator4() {
let i = 0;
let n = 1;
while (true) {
n = yield n * i;
i++;
}
}
let gen4 = generator4();
for (let n of [1, 2, 3, 4, 5]) {
if (Math.random() > 0.5) {
gen4.throw(new Error('模拟异常'));
}
console.log(gen4.next(n).value);
}
这里的gen4.throw(new Error('模拟异常'))模拟外部程序产生了异常,并将异常抛给等待传入参数的生成器函数。
此时生成器函数中缺少异常处理代码,因此异常会抛出给外部程序,最终导致程序停止。
在生成器函数中捕获并处理异常:
function* generator4() {
let i = 0;
let n = 1;
while (true) {
try{
n = yield n * i;
}
catch (e) {
console.log(e);
}
i++;
}
}
return
对于可以无限产出值的生成器函数,可以使用return()方法提前结束:
function* generator5() {
let i = 0;
let n = 1;
while (true) {
n = yield n * i;
i++;
}
}
let gen5 = generator5();
for (let n of [1, 2, 3, 4, 5]) {
console.log(gen5.next(n).value);
}
gen5.return('done');
console.log(gen5.next());
return(...)方法的参数是可选的,如果有参数,就相当于让生成器函数返回了一个指定的值。
异步
异步迭代
有时候,在实现迭代器时next()方法中存在异步请求(比如常见的网络请求),通常这些异步请求返回的结果是我们前面所说的Promise对象,我们已经知道,处理Promise对象的途径之一是使用async...await,因此可以实现异步迭代:
let randomNumber = {
size: 5,
[Symbol.asyncIterator]() {
return {
current: 0,
last: this.size - 1,
async next() {
let num = await new Promise(resolve => setTimeout(resolve, 1000, Math.random()));
if (this.current <= this.last) {
this.current++;
return { done: false, value: num };
} else {
return { done: true };
}
}
}
}
}
async function testRandomNumber() {
for await (let n of randomNumber) {
console.log(n);
}
}
testRandomNumber();
// 0.8102974935193057
// 0.032189090795476494
// 0.634830641483874
// 0.9304805121107931
// 0.6579102151359983
这个示例实现了一个产生随机数的异步迭代器,迭代器内部产生随机数的部分是异步的(setTimeout),可以很容易地用远程调用替换这部分。
如前面所说,为了能处理Promise返回的结果,这里让next方法变成了异步方法(async),此外,返回异步迭代器的对象属性不再是Symbol.iterator而是Symbol.asyncIterator。
最后,对于异步迭代器,使用的时候需要使用for await ... of进行遍历。
异步生成器
async function* generator6(n) {
for (let i = 0; i < n; i++) {
let result = await new Promise(resolve => setTimeout(resolve, 1000, Math.random()));
yield result;
}
}
async function testAsncGenerator() {
for await (let n of generator6(5)) {
console.log(n);
}
}
testAsncGenerator();
// 0.007971594423372474
// 0.5708672336224248
// 0.5406785133665706
// 0.4951318979970558
// 0.4768186280156048
简化迭代
前面说过,使用生成器函数可以简化迭代器的实现,同样的,可以使用异步生成器函数简化异步迭代器的实现:
let randomNumber2 = {
size: 5,
async *[Symbol.asyncIterator]() {
for (let i = 0; i < this.size; i++) {
yield await new Promise(resolve => setTimeout(resolve, 1000, Math.random()));
}
}
}
async function testRandomNumber2() {
for await (let n of randomNumber2) {
console.log(n);
}
}
testRandomNumber2();
示例
async function* fetchCommits(repo){
let url = `https://api.github.com/repos/${repo}/commits`;
while(url){
let result = await fetch(url,{
headers: {'User-Agent': 'Our script'},
});
let body = await result.json();
let nextPage = result.headers.get('Link').match(/<(.*?)>; rel="next"/);
nextPage = nextPage?.[1];
url = nextPage;
for(let commit of body){
yield commit;
}
}
}
async function testFetchCommits() {
let count = 0;
for await (let commit of fetchCommits('javascript-tutorial/en.javascript.info')) {
console.log(commit.author.login);
count++;
if (count >= 10) break;
}
}
testFetchCommits();
这个例子使用异步生成器函数,实现了从 Github 仓库读取commit列表,并在需要翻页时进行翻页,最棒的地方在于,所有这些复杂实现(网络请求和翻页逻辑)都是封装在生成器函数内部的,对于客户端程序,生成器函数只通过yield一次产生一条commit记录,客户端程序使用简单的for of就可以遍历所有的commit。
示例中的
fetch方法要求较新的浏览器或者版本号 >=18 的 Node.js。
The End.
本文所有的示例代码可以从
参考资料

文章评论