红茶的个人站点

  • 首页
  • 专栏
  • 开发工具
  • 其它
  • 隐私政策
Awalon
Talk is cheap,show me the code.
  1. 首页
  2. 前端学习笔记
  3. 正文

JavaScript 学习笔记 8:生成器

2026年4月15日 5点热度 0人点赞 0条评论

生成器

生成器函数

可以使用生成器函数返回一系列值:

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.

本文所有的示例代码可以从这里获取。

参考资料

  • 现代 JavaScript 教程

 

本作品采用 知识共享署名 4.0 国际许可协议 进行许可
标签: 暂无
最后更新:2026年4月15日

魔芋红茶

加一点PHP,加一点Go,加一点Python......

点赞
< 上一篇

文章评论

razz evil exclaim smile redface biggrin eek confused idea lol mad twisted rolleyes wink cool arrow neutral cry mrgreen drooling persevering
取消回复

COPYRIGHT © 2021 icexmoon.cn. ALL RIGHTS RESERVED.
本网站由提供CDN加速/云存储服务

Theme Kratos Made By Seaton Jiang

宁ICP备2021001508号

宁公网安备64040202000141号