JavaScript中的Generator函数

Posted Tyler‘s Blog

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JavaScript中的Generator函数相关的知识,希望对你有一定的参考价值。

1. 简介

Generator函数时ES6提供的一种异步编程解决方案。Generator语法行为和普通函数完全不同,我们可以把Generator理解为一个包含了多个内部状态的状态机

执行Generator函数回返回一个遍历器对象,也就是说Generator函数除了提供状态机,还可以生成遍历器对象。Generator可以此返回多个遍历器对象,通过这个对象可以访问到Generator函数内部的多个状态。

形式上Generator函数和普通的函数有两点不同,一是function关键字后面函数名前面有一个星花符号“*”,二是,函数体内部使用yield定义(生产)不同的内部状态

执行Generator函数返回的是一个遍历器对象,这个对象上有一个next方法,执行next方法会返回一个对象,这个对象上有两个属性,一个是value,是yield关键字后面的表达式的值,一个是done布尔类型true表示没有遇到return语句,可以继续往下执行,false表示遇到return语句。来看下面的语句:

    function* helloWorldGenerator () {
        yield \'hello\';
        yield \'world\';
        return \'ending\';
    }

    var hw = helloWorldGenerator();
    console.log(hw.next()); //第一次调用,Generator函数开始执行,直到遇到yield表达式为止。next方法返回一个对象,它的value属性就是当前yield语句后面表达式的值hello,done属性为false,表示遍历还没有结束
    console.log(hw.next()); //第二次调用,Generator函数从上次yield表达式停下的地方,一直执行到下一个yield表达式。next方法返回的对象的value属性就是当前yield语句后面表达式的值world,done属性值为false,表示遍历还没有结束。
    console.log(hw.next()); //第三次调用,Generator函数从上次yield表达式停下的地方,一直执行到return语句(如果没有return语句,则value属性为undefined),done属性为true,表示遍历已经执行结束。
    console.log(hw.next()); //第四次调用,此时Generator函数已经执行完毕,next方法返回对戏那个的value属性为undefined,done属性为true,表示遍历结束。
    console.log(hw.next()); //第五次执行和第四次执行的结果是一样的。 

执行结果如下图:

1. 定义Generator函数helloWorldGenerator函数
2. 函数内部有2个yield表达式和一个return语句,return语句结束执行
3. Generator函数的调用方法和普通函数一样,也是在函数名后面加上一对圆括号。不同的是调用之后,函数不是立即执行,返回的也不是return语句的结果undefined,而是一个指向内部状态的指针对象,也就是上面说的遍历器对象(Iterator Object)
4. 调用遍历器对象的next方法,状态指针移动到下一个状态,返回{value: "hello", done: false}
5. 调用遍历器对象的next方法,状态指针移动到下一个状态,返回{value: "world", done: false}
6. 调用遍历器对象的next方法,状态指针移动到下一个状态,返回{value: "ending", done: true},done为true,说明已经遇到了return语句,后面已经没有状态可以返回了
7. 调用遍历器对象的next方法,指针不再移动,返回{value: undefined, done: true}
8. 调用遍历器对象的next方法,指针不再移动,返回{value: undefined, done: true}

注意yield表达式后面的表达式,只有当调用next方法,内部指针指向该语句时才会执行,相当于javascript提供了手动的“惰性求值”语法功能。

function* gen() {
  yield  123 + 456;
} 

上面代码中,yield后面表达式123 + 456,不会立即求值,只会在next方法将指针移动到这一句时,才会求值。

yield表达式语句和return语句有相似之处,也有却别。相似的地方是都能返回紧跟在语句后面的那个表达式的值。却别在于每次遇到yield,函数暂停执行,下一次再从该位置继续向后执行,return语句没有位置记忆功能。一个函数里面,只能执行一次return语句,但是可以多次执行yield表达式。也就是说Generator可以逐次生成多个返回值,这也是它的名字的来历。 

Generator函数中可以不用yield表达式,这时就变成了一个单纯的暂缓执行函数。看下面代码:

    function* f () {
        console.log(\'执行了!\')
    }
    var generator = f();
    setTimeout(function () {
        console.log(generator.next()); // 执行Generator函数,只到遇到yield表达式,这里没有就直接输出:"执行了!",函数返回{"done":true}没有value
    }, 2000);

输出结果如下:

Generator函数f()中没有yield表达式,但是仍然还是一个Generator函数。如果函数f是一个普通函数,在执行var generator = f();的时候就会输出“执行了!”。但是f()是一个Generator函数,就变成了只有调用next方法的时候,函数f才会执行。

另外需要注意,yield表达式只能用在Generator函数里面,用在其他地方都会报错。看下面的代码:

    var arr = [1, [[2, 3], 4], [5, 6]];
    var flat = function* (a) {
        a.forEach(function (item) {
            if (typeof item !== \'number\') {
                yield * flat(item)
            } else {
                yield item
            }
        })
    }
    for (let f of flat) {
        console.log(f);
    } 

上面代码会报错,因为forEach方法的参数是一个普通函数,但是在里面使用了yield表达式。可以把forEach改成for循环 

    var arr = [1, [[2, 3], 4], [5, 6]];
    var flat = function* (a) {
        var length = a.length;
        for (var i = 0; i < length; i++) {
            var item = a[i];
            if (typeof item !== \'number\') {
                yield *flat(item)
            } else {
                yield item;
            }
        }
    }
    for (var f of flat(arr)) {
        console.log(f);
    }

 输出结果如下:

另外,如果yield表达式用在另外一个表达式之中,必须放在圆括号内部。如下:

     function *demo() {
       console.log(\'hello \' + (yield));
       console.log(\'world \' + (yield  123));
     }
     var gen = demo();
     console.log(gen.next());
     console.log(gen.next());
     console.log(gen.next());

输出结果如下:

1.  定义Generator函数demo
2.  函数内部有输出"hello"+(yield)和“world”+(yield 123)
3.  调用demo方法得到遍历器对象gen
4.  调用遍历器对象的next方法并输出,注意先执行表达式语句“hello” + (yield),得到{value: undefined, done: false},再输出:“hello undefined”。注意直接输出yield表达式得到的结果是undefined,必须使用遍历器对象的next方法才能获取yield表达式后面的值
5.  调用遍历器对象的next方法并输出,注意先执行表达式语句“worold” + (yield),得到{value: 123, done: false},再输出:“world undefined”。注意直接输出yield表达式得到的结果是undefined,必须使用遍历器对象的next方法才能获取yield表达式后面的值
6.  调用遍历器对象的next方法,因为后面已经没有yield表达式,虽然没有return语句,判断依据是否有更多的yield语句为标准,还是输出{value: undefined, done: true}。done的值是true。后面无论调用next方法多少次,都是这个结果。

yield表达式用作函数或者放在赋值表达式的右边,可以不加括号。如下:

function* demo() {
  foo(yield \'a\', yield \'b\'); // OK
  let input = yield; // OK
}

上面说到yield表达式本身输出的是undefined,也就是说yield表达式本身没有返回值,或者说总是返回undefined。next方法可以带一个参数,该参数会被当做上一个yield表达式的返回值。

     function *f() {
       for(var i=0; true; i++){
        var reset = yield i;
        if(reset) { i = -1 }
       }
     }
     var g = f();
     console.log(g.next());
     console.log(g.next());
     console.log(g.next(true));

 返回结果如下:

上面代码返回一个可以无限运行的Generator函数f,如果next方法没有参数,每次运行到yield表达式,变量reset的值总是yield表达式的值undefined。当next方法带一个参数true时,变量reset就被重置为这个参数的值,即true,因此i的值会等于-1,下一轮循环就会从-1开始递增。这个功能有很重要的语法意义。Generator函数从暂停状态到恢复运行,它的上下文状态(context)是不变的。通过next方法的参数,就有办法在Generator函数开始运行之后,继续向函数体内部注入值。也就是说,在Generator函数运行的不同阶段,从外部向内部注入不同的值,可以调整函数行为。

看下面的例子:

    function* foo (x) {
        var y = 2 * (yield (x + 1));
        var z = yield (y / 3);
        return (x + y + z);
    }
    var a = foo(5);
    console.log(a.next());
    console.log(a.next());
    console.log(a.next());
    var b = foo(5);
    console.log(b.next());
    console.log(b.next(12));
    console.log(b.next(13));

运行结果如下图:

1. 申明一个Generator函数foo
2. 调用函数foo,传入参数5,得到遍历器对象a
3. 调用遍历器对象a的next方法,返回yield关键字后面表达式(x + 1)的值,得到6,返回结果{ value: 6, done: false }。
4. 调用遍历器对象a的next方法,往下执行,因为执行next的时候没带参数,上一次yield表达式的值从6变成undefined,而不是6,y的值是2 * undefined,即为NaN。本次yield表达式的值是undefined / 3 为NaN。最后返回结果{ value: undefined, done: false }
5. 调用遍历器对象a的next方法,往下执行,因为执行next的时候没有带参数,上一次yield表达式的值为从NaN变成undefined,因此z的值是undefined,返回的值为5 + NaN + undefined,即为NaN
6. 调用函数foo,传入参数5,得到遍历器对象b
7. 调用遍历器对象的next方法,返回yield关键字后面表达式(x + 1)的值,得到6,返回结果{ value: 6, done: false }
8. 调用遍历器对象的next方法,传参12,因此上一次yield关键字后面的表达式的(x + 1)的值从6变为12,y的值是2 * 12,即为24。yield关键字后面表达式的值为 (24 / 3),即为8。最后返回结果{ value: 8, done: false }
9. 调用遍历器对象的next方法,传入参数13,因此上一次yield关键字后面的表达式(y / 3)的值从8变成13,z的值是13。表达式(x + y + z)的值是(5 + 24 + 13),即42。最后返回结果{ value: 24, done: true }

注意,由于next方法的参数表示上一个yield表达式的返回值,所以在第一调用next方法时,传递参数是无效的。JavaScript引擎直接忽略第一次使用next方法时的参数,只有从第二次使用next方法开始,参数才是有效的。从语义上说,第一个next方法用来启动遍历器对象,所以不用带参数。

再看一个例子:

    function * dataConsumer () {
        console.log(\'started\')
        console.log(`1.${yield }`)
        console.log(`2.${yield }`)
        return \'result\'
    }
    let genObj = dataConsumer()
    genObj.next()
    genObj.next(\'a\')
    genObj.next(\'b\')

 输出结果:

1. 定义Generator函数dataConsumer
2. 调用dataConsumer函数,得到遍历器对象genObj
3. 调用遍历器对象genObj的next方法,执行执行dataConsumer函数,只到遇到yield表达式为止。注意第一句输出“started”,第二句里就有yield表达式,因此在这里停止。最终结果是“started”
4. 调用遍历器对象genObj的next方法,传入参数‘a’,继续往下执行,上一次yield表达式的值从undefined变成‘a’,最后输出1.a
5. 调用遍历器对象genObj的next方法,传入参数‘b’,继续往下执行,上一次yield表达式的值从undefined变成‘b’,最后输出2.b

上面代码是一个很直观的例子,每次通过next方法向Generator函数注入值,然后打印出来。

如果想要第一次调用next方法时就能够输入值,可以在Generator函数外面再包一层。

    function wrapper (generatorFunction) {
        return function (...args) {
            let generatorObject = generatorFunction(...args)
            generatorObject.next()
            return generatorObject
        }
    }
    const wrapped = wrapper(function *() {
        console.log(`first input: ${yield }`)
        return \'DONE\'
    })
    wrapped().next(\'hello\')

 输出结果:

上面代码中,Generator函数如果不用wrapper先包一层,是无法在第一次调用next方法的时候就输入参数的。 这个其实在包装函数wrapper里面已经先执行了一次next方法了。

2. Generator和Iterator接口的关系

任意一对象的Symbol.iterator方法,等于该对象的遍历器生成函数,调用该函数会返回该对象的一个遍历器对象。

由于Generator函数就是遍历器生成函数,因此可以把Generator赋值给对象的Symbol.iterator属性,从而使这个对象具有Iterator接口

     var myIterable = {};
     myIterable[Symbol.iterator] = function *() {
       yield 1;
       yield 2;
       yield 3;
     };
     console.log([...myIterable]);

输出结果如下:

上面代码中Generator函数赋值给Symbol.iterator属性,从而使myIterator对象具有了iterator接口,这样就可以被...运算符遍历了。

Generator函数执行后,返回一个遍历器对象。该对象本身也具有Symbol.iterator属性,执行后返回自身。

     function *gen() {
     }
     var g = gen();
     console.log(g[Symbol.iterator]() === g); // 输出true

 上面代码中,gen是一个Generator函数,调用它会生成一个遍历器对象g,它的Symbol.iterator属性也是一个遍历器对象生成函数,执行后返回它自己。 

for...of循环可以自动遍历Generator函数生成的Generator对象,并且不需要调用next方法,看下面的代码:

    function* foo () {
        yield 1;
        yield 2;
        yield 3;
        yield 4;
        yield 5;
        return 6;
    }
    for (let v of foo()) {
        console.log(v);
    }

 输出结果如下:

上面代码中使用for...of循环,以此显示5个yield表达式的值。这里需要注意,一旦next方法的返回对象的done属性为true,for...of循环就会终止,且不包含该返回对象,所以上面代码中return语句返回值6,不包括在for...of循环中。

下面是一个利用Generator函数和for...of循环,实现斐波那契数列的例子

    function* fibonacci () {
        let [prev, curr] = [0, 1];
        for (; ;) {
            [prev, curr] = [curr, prev + curr];
            yield curr;
        }
    }
    for (let n of fibonacci()) {
        if (n > 1000) break;
        console.log(n);
    }

输出结果如下:

从上面代码可以看出,使用for...of语句时,不再需要使用next方法。注意这里用for...of循环代替了next方法,照样可以执行Generator函数。

利用for...of循环,可以写出遍历任意对象(Object)的方法。原生的JavaScript兑现更没有遍历接口,无法使用for...of循环,通过Generator函数为它加上这个接口,就可以使用了。

    function* objectEntries (obj) {
        let propKeys = Reflect.ownKeys(obj);

        for (let propKey of propKeys) {
            yield [propKey, obj[propKey]];
        }
    }

    let jane = {first: \'Jane\', last: \'Doe\'};
    for (let [key, value] of objectEntries(jane)) {
        console.log(`${key}: ${value}`);
    }

 输出结果如下:

上面代码中,原生对象jane不具备Iterator接口,无法用for...of遍历。这是我们通过Generator函数objectEntries为它加上遍历器接口,就可以用for...of遍历了。加上遍历器接口的另一种写法是,将Generator函数加到对象的Symbol.iterator属性上,代码如下:

    function* objectEntries () {
        let propKeys = Object.keys(this);
        for (let propKey of propKeys) {
            yield [propKey, this[propKey]];
        }
    }

    let jane = {first: \'Jane\', last: \'Doe\'};
    jane[Symbol.iterator] = objectEntries;
    for (let [key, value] of jane) {
        console.log(`${key}: ${value}`);
    }

 输出结果如下:

除了for...of循环扩展运算符(...)解构赋值Array.from方法内部调用的都是遍历器接口,这就是说,它们都可以将Generator函数返回的Iterator对象作为参数。看下面的代码:

    function* numbers () {
        yield 1
        yield 2
        return 3
        yield 4
    }
    // 扩展运算符
    console.log(...numbers())
    // Array.from方法
    console.log(Array.from(numbers()))
    // 解构赋值
    let [x, y] = numbers()
    console.log(x, y)
    // for ... of循环
    for (let n of numbers()) {
        console.log(n)
    }

输出结果如下:

3. Generator.property上的方法

3.1. Generator.property.throw()

Generator原型对象上有一个throw方法,可以在函数体外抛出错误,然后在Generator函数体内捕获。

    var g = function* () {
        try {
            yield;
        } catch (e) {
            console.log(\'内部捕获\', e)
        }
    }
    var i = g();
    i.next();
    try {
        i.throw(\'a\');
        i.throw(\'b\');
    } catch (e) {
        console.log(\'外部捕获\', e);
    }

 输出结果如下:

上面代码中,遍历器对象i连续抛出两个错误。第一个错误被Generator函数体内部的catch语句捕获。i第二次抛出错误,由于Generator函数内部的catch语句已经执行过了,不会再捕捉到这个错误,所以这个错误就被抛出了Generator函数体,被函数体外的catch语句捕获。

throw方法可以接受一个参数,参数会被catch语句接收,建议抛出Error对象实例。

    var g = function* () {
        try {
            yield;
        } catch (e) {
            console.log(e)
        }
    }
    var i = g();
    i.next();
    i.throw(new Error(\'出错了!\'))

 输出结果如下:

不要混淆遍历器对象的throw方法和全局的throw命令。上面代码的错误,是用遍历器对象的throw方法抛出的,而不是用throw命令抛出的。后者只能被函数体外的catch语句捕获。

    var g = function* () {
        while (true) {
            try {
                yield;
            } catch (e) {
                if (e !== \'a\') {
                    throw e;
                }
                console.log(\'内部捕获\', e)
            }
        }
    }
    var i = g();
    i.next();
    try {
        throw new Error(\'a\');
        throw new Error(\'b\');
    } catch (e) {
        console.log(\'外部捕获\', e);
    }

 输出结果如下:

上面代码只捕获了a,是因为函数体外的catch语句块,捕获了抛出的a错误以后,就不会再继续try代码块里剩余的语句了。因为没有执行i.catch()语句,内部的异常不会被捕获。

如果Generator函数内部没有try...catch代码块,那么throw方法抛出的错误将被外部try...catch代码块捕获。

    var gen = function* gen () {
        yield console.log(\'hello\');
        yield console.log(\'world\');
    }

    var g = gen();
    g.next();
    g.throw();

 输出如下:

上面代码中给,g.throw抛出错误后,没有任何try...catch代码可以捕获这个错误,导致程序报错,终端执行。

throw方法抛出的错误要被内部捕获,前提是必须至少执行一次next方法。

    function * gen () {
        try {
            yield  1
        } catch (e) {
            console.log(\'内部捕获\')
        }
    }
    var g = gen()
    t.throw(1)

 输出结果如下:

上面代码中,g.throw(1)执行时,next方法一次都没有执行过。这时,抛出的错误不会被内部捕获,而是直接在外部抛出,导致程序出错。这种行为其实很好理解,因为第一次执行next方法,等同于启动执行Generator函数的内部代码,否则Generator函数还没有开始执行,这时throw方法抛出错误只能抛出在函数外部。

throw方法被捕获以后,会附带执行下一条yield表达式。也就是说,会附带执行一次next方法。

    var gen = function* gen () {
        try {
            yield console.log(\'a\');
        } catch (e) {

        }
        yield console.log(\'b\');
        yield console.log(\'c\');
    }
    var g = gen();
    g.next();
    g.throw();
    g.next();

 输出结果如下:

上面代码中,g.throw方法被捕获以后,自动执行了一次next方法。所以会打印b。另外,也可以看到,只要Generator函数内部部署了try...catch代码块,那么遍历器的throw方法抛出的错误,不影响下一次遍历。

另外,throw命令和g.throw方法是无关的,两者互不影响。

    var gen = function* gen() {
        yield console.log(\'hello\');
        yield console.log(\'world\');
    }
    var g = gen();
    g.next();

    try {
        throw new Error();
    } catch (e) {
        g.next();
    }

 输出结果如下:

上面代码中,throw命令抛出的错误不会影响到遍历器的状态,所以两次执行next方法,都进行了正确的操作。

这种函数体内捕获错误的机制,方便了对错误的处理。多个yield表达式可以只用一个try...catch代码块来捕获错误。如果使用回调函数的写法,想要捕获多个错误,就不得不为每个函数内部写一个错误处理语句,现在只在Generator函数内部洗写一次catch语句就可以了。

Generator函数体外抛出的错误,可以在函数体内捕获;反过来,Generator函数体内抛出的错误,也可被函数体外的catch捕获。

    function* foo () {
        var x = yield 3;
        var y = x.toUpperCase();
        yield y;
    }

    var it = foo();
    it.next();
    try {
        it.next(42);
    } catch (err) {
        console.log(err);
    } 

上面代码中,第二个next方法向函数体内传入一个参数42,,数值是没有toUpperCase方法的,所以会抛出一个TypeError错误,被函数体外的catch捕获。

一旦Generator执行过程中抛出错误,且没有被内部捕获,就不会再往下执行下去了。如果还调用next方法,将返回一个value属性为undefined,done属性为tru的对象,即JavaScript引擎认为这个Generator已经运行结束了。

     function *g() {
       yield 1;
       console.log(\'throwing an exception\');
       throw new Error(\'generator broke!\');
       yield 2;
       yield 3;
     }
     function log(generator) {
       var v;
       console.log(\'starting generator\');
       try{
           v = generator.next();
           console.log(\'第一次运行next方法\', v);
       } catch(err) {
           console.log(\'捕捉错误\', v);
       }
       try{
           v = generator.next();
           console.log(\'第二次运行next方法\',v);
       }catch(err){
         console.log(\'捕捉错误\', v);
       }
       try{
           v = generator.next();
           console.log(\'第三次运行next方法\', v);
       } catch(err) {
           console.log(\'捕捉错误\', v);
       }
       console.log(\'caller done\');
     }
     log(g());

 执行结果如下:

上面代码一共三次运行next方法,第二次运行的时候会抛出错误,然后第三次运行的时候,Generator函数就已经结束了,不再执行下去。

3.2.Generator.property.return()

Generator函数返回的遍历器对象,还有一个return方法,可以返回给定值,并且终结遍历Generator函数。

     function *gen() {
       yield 1;
       yield 2;
       yield 3;
     }
     var g = gen();
     console.log(g.next());
     console.log(g.return(\'foo\'));
     console.log(g.next());

 执行结果如下:

上面代码中,遍历器对象g调用return方法之后,返回值的value属性就是return方法的参数“foo”。并且,Generator函数的遍历就终止了,返回值的done属性为true,以后再调用next方法,done属性的返回值总是true。

如果return方法调用时,不提供参数,则返回值的value属性为undefined。

    function *gen() {
        yield 1;
        yield 2;
        yield 3;
    }
    var g = gen();
    console.log(g.next());
    console.log(g.return());

 执行结果如下:

如果Generator函数内部有try...finally代码块,那么return方法会推迟到finally代码块执行完后再执行。

     function * numbers() {
         yield 1;
         try{
             yield 2;
             yield 3;
       } finally {
             yield 4;
             yield 5;
       }
       yield 6;
     }
     var g = numbers();
     console.log(g.next());
     console.log(g.next());
     console.log(g.return(7));
     console.log(g.next());
     console.log(g.next());

 执行结果如下:

上面代码中,调用return方法后,就开始执行finally代码块,然后等到finally代码块执行完,再执行return方法。 遇到return语句对象指针就会跳转到finally里去执行,只到把finally里的语句执行完再执行return语句。

next(),throw(),return()方法的共同点

next(),throw(),return()这三个方法本质上是同一件事情,可以放在一起理解。他们的作用个都是让Generator函数恢复执行并且使用不同的语句替换yield表达式

next()是将yield表达式替换成一个值。

    const g = function* (x, y) {
            let result = yield x + y
            return result
        }
    const gen = g(1, 2)
    console.log(gen.next())
    console.log(gen.next(1))

 输出结果如下:

上面代码中,第二个next(1)方法相当于将yield表达式x + y替换成一个值1。如果next方法没有参数,就相当于替换成undefined。所以第二次调用next方法的时候如果不传参数,返回的结果是{ value: undefined, done: false }。

throw是将yield表达式替换成一个throw语句。

    const g = function* (x, y) {
            let result = yield x + y
            return result
        }
    const gen = g(1, 2)
    console.log(gen.next())
    gen.throw(new Error(\'出错了\'))

输出结果如下:

上面代码相当于将let result = yield x + y替换成let result = throw(new Error(\'出错了\'))

return语句时将yield表达式替换成一个return语句

    const g = function* (x, y) {
        let result = yield x + y
        return result
    }
    const gen = g(1, 2)
    console.log(gen.next())
    console.log(gen.return(2))

输出结果如下:

return语句相当于将let result = yield x + y替换成let result = return 2

4. yield*表达式

如果在Generator函数内部,调用跟另外一个Generator函数,默认情况下是没有效果的。看下面代码:

    function* foo () {
        yield \'a\';
        yield \'b\';
    }

    function* bar () {
        yield \'x\';
        foo();
        yield \'y\';
    }

    for (let v of bar()) {
        console.log(v);
    }

输出结果如下 

上面代码中,foo和bar都是Generator函数,在bar函数中调用foo,是不会有任何效果的。可以使用yield*表达式来调用另外一个Generator函数。如下代码:

    function* foo () {
        yield \'a\';
        yield \'b\';
    }

    function* bar () {
        yield \'x\';
        yield *foo();
        yield \'y\';
    }

    for (let v of bar()) {
        console.log(v);
    }

执行效果如下:

function* bar() {
  yield \'x\';
  yield* foo();
  yield \'y\';
}

// 等同于
function* bar() {
  yield \'x\';
  yield \'a\';
  yield \'b\';
  yield \'y\';
}

// 等同于
function* bar() {
  yield \'x\';
  for (let v of foo()) {
    yield v;
  }
  yield \'y\';
}

for (let v of bar()){
  console.log(v);
}

输出结果是相同的,在一个Generator函数中使用yield*调用另外一个Generator函数,相当于把另一个Generator函数中的yield表达式放在这个函数中执行。

    function* inner () {
        yield \'hello!\';
    }

    function* outter1 () {
        yield \'open\';
        yield inner();
        yield \'close\';
    }

    var gen = outter1();
    console.log(gen.next().value);
    console.log(gen.next().value);
    console.log(gen.next().value);

    function* outter2 () {
        yield \'open\';
        yield* inner();
        yield \'close\';
    }

    var gen2 = outter2();
    console.log(gen2.next().value);
    console.log(gen2.next().value);
    console.log(gen2.next().value);

输出结果如下:

上面代码中,outer2使用了yield*表达式,outer1没有使用。结果就是,outer1返回一个遍历器对象,outer2返回该遍历器对象的内部值。从语法角度看,如果yield表达式后面跟的是一个遍历器对象,需要在yield关键字后面加上星号,表明它返回的是一个遍历器对象,这被称为yield*表达式。

    let delegatedIterator = (function* () {
        yield \'Hello!\';
        yield \'Bye!\';
    }());

    let delegatingIterator = (function* () {
        yield \'Greetings!\';
        yield* delegatedIterator;
        yield \'Ok, bye.\';
    }());

    for (let value of delegatingIterator) {
        console.log(value);
    }

执行结果如下:

上面代码中,delegatingIterator是代理者,delegatedIterator是被代理者,由于yield* delegatedIterator语句得到的值,是一个遍历器,所以要用星号表示。运行结果是用一个遍历器遍历了多个Generator,有递归的效果。

yield*后面的Generator函数没有return语句的时候,等同于在Generator函数内部,部署了一个for...of循环。如下代码:

     function *concat(iter1, iter2) {
       yield * iter1;
       yield * iter2;
     }
     // 等同于
     function * concat(iter1, iter2) {
       for(var value of iter1){
           yield value;
       }
       for(var value of iter2){
           yield value;
       }
     }

 

上面代码说明,yield*后面的Generator函数(没有return语句时),不过是for...of的一种简写形式,完全可以用后者代替前者。反之,在有return语句的时候需要使用var value = yield* iterator的形式获取return语句的值。

如果yield*后面跟着一个数组,由于数组原生支持遍历器,因此就会遍历数组成员。如下代码:

    function* gen () {
        yield* [\'a\', \'b\', \'c\']
    }
    let g = gen()
    console.log(g.next())
    console.log(g.next())
    console.log(g.next())
    console.log(g.next())

执行结果如下:

上面代码中,yield命令后面如果不加星号,返回的是整个数组,加了星号就表示返回的是数组的遍历器对象。

实际上,任何数据结构,只要有Iterator接口,就可以被yield*表达式遍历。

    let read = (function* () {
        yield \'hello\';
        yield* \'world\';
    })();
    console.log(read.next().value);
    console.log(read.next().value);

返回结果如下:

上面代码中,yield表达式返回的是整个字符串,但是yield*表达式返回的是单个字符。因为字符串有Iterator接口,所以被yield*表达式遍历。

如果被代理的Generator函数有return语句,那么就可以向代理它的Generator函数返回数据。

    function* foo () {
        yield 2;
        yield 3;
        return "foo";
        yield 4;
    }

    function* bar () {
        yield 1;
        var v = yield* foo();
        console.log("v: " + v);
        yield 5;
    }

    var it = bar();
    console.log(it.next()); // {value: 1, done: false}
    console.log(it.next()); // {value: 2, done: false}
    console.log(it.next()); // {value: 3, done: false}
    console.log(it.next()); // "v: foo" {value: 5, done: true}
    console.log(it.next()); // {value: undefined, done: true} 

执行结果如下:

1. 定义Generator函数foo
2. 定义Generator函数bar,在函数内部使用yield*表达式调用函数foo
3. 调用bar方法,得到遍历器对象it
4. 调用遍历器对象it的next方法,返回{ value: 1, done: false }
5. 调用遍历器对象it的next方法,返回Generator函数foo的第一个yield表达式返回的对象{ value: 2, done: false }
6. 调用遍历器对象it的next方法,返回Generator函数foo的第二个yield表达式返回的对象{ value: 3, done: false }
7. 调用遍历器对象it的next方法,foo结束,foo方法里面有return语句,返回值是“foo”,继续往下执行只到遇到yield语句,输出“v:foo” 并输出{ value: 5, done: false }
8. 调用遍历器对象it的next方法,Generator函数里已经没有yield语句,输出{ value: undefiined, done: true }

再看下面的例子

    function* genFuncWithReturn () {
        yield \'a\';
        yield \'b\';
        return \'The result\';
    }

    function* logReturned (genObj) {
  

以上是关于JavaScript中的Generator函数的主要内容,如果未能解决你的问题,请参考以下文章

javascript异步编程之generator(生成器函数)与asnyc/await语法糖

JavaScript的ES6语法11generator实例-KOA

浅谈JavaScript中的异步处理

初步学习javascript的generator函数

一次搞懂 Generator 函数

JavaScript的ES6语法9generator函数之yield关键字