愿你坚持不懈,努力进步,进阶成自己理想的人

—— 2017.09, 写给3年后的自己

《ECMAScript6入门》学习笔记之Generator的语法

一、介绍

generator是ES6提供的一种异步编程解决方案,从语法上,可以将其理解为一个状态机,封装了多个内部状态。
执行generator函数,会返回一个遍历器对象,所以 generator也是一个遍历器对象生成函数,返回的遍历器对象,可以遍历generator函数内部的每一个状态,定义一个generator函数如:

function* gen() {
    yield 1;
    yield 2;
    return 3;
}

即:使用function*来声明,且内部使用yield(产出的意思)关键字。
generator函数与普通函数的调用方法一样,不同的是:调用generator函数后,该函数并不执行,而是返回一个遍历器对象,即:

const it = gen();

此后,需要调用遍历器对象next()方法,从而使得指针移动到下一个状态。这里的过程为:

每调用一次next(),内部指针就从函数头部上一次停下的地方开始执行,直到遇到下一个yield或者return为止

所以:yield相当于暂停执行,next()相当于继续执行


二、yield表达式

yield表达式相当于是一个暂停标志,所以generator中,next()的运行机制为:
1)遇到yield,就暂停后面的操作,然后将yield紧跟的表达式的值,作为next()的返回对象中value属性的值
2)下一次调用next()时,会继续往下执行,直到再遇到yield,就使用(1)的策略
3)下一次调用next()时,仍然是继续执行,如果没有遇到yield,就一直运行到函数结束,直到遇到return语句为止,并将return接的表达式的值,作为next()返回对象中value属性的值
4)如果没有return语句,则相当于return undefined,所以next()返回对象中的value的值为undefined
使用generator,可以实现惰性求值,因为generator需要先“启动”,而后调用next()方法才能执行,如:

function* gen() {
    console.log('Hello, world');
}
const g = gen(); // 此时没有任何输出
g.next(); // 输出 'Hello, world'

由于yield关键字只能用于generator函数内部,所以若普通函数使用了yield关键字,那么会报错:

function foo() {
    yield 1;
}
// SyntaxError: Unexpected number

类似的,如果在forEach()里使用yield,也会报错,如实现flatten函数:

const arr = [1, [[2, 3], 4], [5, 6]];
function* flatten(arr) {
    arr.forEach(function(item) {
        if (typeof item !== 'number') {
            yield* flatten(item);
        } else {
            yield item;
        }
    });
}
// SyntaxError: Unexpected number

解决的办法是:使用for或者for-of等语句代替:

const arr = [1, [[2, 3], 4], [5, 6]];
function* flatten(a) {
    for (let item of a) {
        if (typeof item !== 'number') {
            yield* flatten(item);
        } else {
            yield item;
        }
    }
}
const g = flatten(arr);
[...g]; // 输出:[1, 2, 3, 4, 5, 6]

如果yield表达式放在另一个表达式里面,就需要用()包起来,如:

function* gen() {
    console.log('Hello' + yield 'World');
}
// SyntaxError: Unexpected identifier

此时就应该这么写:

function* gen() {
    console.log('Hello' + (yield 'World'));
}

不过,如果yield表达式作为函数参数或者放在赋值表达式=的右边,可以不需要括号:

function* gen() {
    console.log('Hello', yield 'World');
}


三、和Iterator接口的关系

generator函数是遍历器生成函数,所以可以把generator函数赋值给对象的Symbol.iterator属性,从而使得对象拥有iterator接口:

const obj = {};
obj[Symbol.iterator] = function* () {
    yield 1;
    yield 2;
    yield 3;
}
[...obj]; // [1, 2, 3]

因为generator函数执行后,会返回一个遍历器对象,所以这个对象会拥有[Symbol.iterator]()方法,执行后,等于自身:

function* gen() {}
const g = gen();
g[Symbol.iterator]() === g;


四、next方法的参数

yield方法本身没有返回值(或者说返回值就是undefined),我们可以通过给next()传值,从而成为yield的返回值,从而实现外部数据 --> 内部数据的传递:

function* gen() {
    const a = yield 1;
    const b = a * (yield 2);
    return b;
}

const g = gen();
g.next(); // { value: 1, done: false }
g.next(5); // { value: 2, done: false }
g.next(10); // { value: 50, done: true }

解读如下:
1)首先执行const g = gen()时,得到一个遍历器对象
2)执行g.next(),启动generator,开始执行generator(此时给g.next()传入的参数是不起作用的,所以像V8会进行优化,直接忽略了),函数开始执行,遇到yield,函数暂停,然后将其后表达式的值返回,所以得到:{ value: 1, done: false }
3)执行g.next(5),函数从上一次yield时停下的地方继续执行,并用参数5替代yield 1,所以这里就相当于const a = 5,此后遇到下一个yield,函数暂停,将其后的表达式值返回,所以得到:{ value: 2, done: false }
4)执行g.next(10),函数从上一次yield时停下的地方继续执行,并用参数10替代yield 2,所以相当于const b = a * 10,所以执行后得到b = 50,然后继续往下执行,没有yield,但是遇到了return,结束迭代,返回{ value: 50, done: true }


五、for-of循环

for-of循环可以自动遍历generator函数生成的Iterator对象,且不需要调用next()方法,如:

function* gen() {
    yield 1;
    yield 2;
    return 3;
}
for (let x of gen()) {
    console.log(x);
}
// 输出:1 2

需要注意的是:由于return 3后,其对应的对象为{ value: 3, done: true },在done为true的情况下,for-of循环就会终止,且不包含改返回对象,所以输出是1 2,而非1 2 3


六、Generator.prototype.throw()

generator提供了一种机制:可以在外部将错误抛入内部,如:

function* gen() {
    try {
        yield;
    } catch(e) {
        console.log('inner:', e);
    }
}

const g = gen();
g.next();
try {
    g.throw('error1');
    g.throw('error2');
} catch(e) {
    console.log('outter:', e);
}
/*
输出:
inner: error1
outter: error2
*/

这里,g.throw('error1')抛入的第一个错误,被generator函数内部捕获了,所以会输出inner: error1,而g.throw('error2')则不能被捕获,因为try-catch块已经执行并处理第一个错误了,这时候错误会抛到外部被捕获,所以输出outter: error2
需要注意的是:throw方法被捕获后,会附带执行下一条yield表达式,即附带执行一次next方法,如:

function* gen() {
    try {
        yield console.log('a');
    } catch(e) {
        console.log('inner: ', e);
    }
    yield console.log('b');
    yield console.log('c');
}
const g = gen();
g.next(); // 输出:a
g.throw(); // 输出:inner: undefined,输出:b
g.next(); // 输出:c

如果generator执行过程中抛出了错误,且没有被内部捕获,那么就不会再继续执行下去了(即认为这个generator已经运行结束了),如:

function* gen() {
    yield 1;
    throw new Error('Error occurred!');
    yield 2;
    yield 3;
}

const g = gen();
try {
    console.log('first time: ', g.next());
} catch(e) {
    console.log('first error: ', e);
}

try {
    console.log('second time: ', g.next());
} catch(e) {
    console.log('second error: ', e);
}

try {
    console.log('third time: ', g.next());
} catch(e) {
    console.log('third error: ', e);
}
/*
输出:
first time: { value: 1, done: false }
second error: Error: Error occurred!
third time: { value: undefined, done: true }
*/

所以,当generator函数体内抛出了错误且没有在内部捕获,generator就会被认为是结束了,此后调用next()方法,返回的都会是{ value: undefined, done: true }
因此,generator函数具有的特征是:函数体外抛出的错误,可以在generator函数内捕获;而genera内抛出的错误,也可以被函数外的catch()捕获,这种机制,可以方便实现用一个try-catch来捕获多个yield表达式中抛出的错误。


六、Generator.prototype.return()

return()方法能够终止一个generator函数的遍历,并且以传入的参数作为遍历的结束值,如:

function* gen() {
    yield 1;
    yield 2;
    yield 3;
}
const g = gen();
g.next(); // { value: 1, done: false }
g.return(4); // { value: 4, done: true }
g.next(); // { value: undefined, done: true }

如果generator内部有try-finally代码块,那么return方法会推迟到finally代码块执行完再执行,如:

function* gen() {
    yield 1;
    try {
        yield 2;
        yield 3;
    } finally {
        yield 4;
        yield 5;
    }
}
const g = gen();
g.next(); // { value: 1, done: false }
g.next(); // { value: 2, done: false }
g.return(7); // { value: 4, done: false }
g.next(); // { value: 5, done: false }
g.next(); // { value: 7, done: true }


七、next()throw()return()共同点

next()throw()return()三个方法本质上做的是同一件事:让generator函数恢复执行,不同的是,它们相当于用不同的语句替换yield表达式,如:
1)next():将yield表达式替换成一个值

function* gen() {
    const a = yield 1;
    console.log(a);
}
const g = gen();
g.next();
g.next('Hello, world');

其中,g.next('Hello, world');执行后相当于:

const a = 'Hello world';

2)throw():将yield表达式替换成一个throw语句

const g = gen();
g.next();
g.throw('error');

其中,g.throw('error')执行后相当于:

const a = throw 'error';

3)return():将yield表达式替换成一个return语句

const g = gen();
g.next();
g.return('HelloWorld')

其中,g.return('HelloWorld')执行后相当于:

const a = return 'HelloWorld'


八、yield *表达式

如果想要在generator函数内部调用另一个generator函数,那么如下这种方式,是没有效果的:

function* foo() {
    yield 1;
}
function* bar() {
    foo();
    yield 2;
}

[...bar()]; // [2]

这种情况下,就需要使用yield*,如下:

function* bar() {
    yield* foo();
    yield 2;
}
[...bar()]; // [1, 2]

如果yield*后面接的generator函数有返回值,那么就可以返回值就可以替代yield*这部分,如:

function* foo() {
    yield 2;
    return 'Hello';
}
function* gen() {
    yield 1;
    console.log(yield* foo());
}
const g = gen();
g.next(); // { value: 1, done: false }
g.next(); // { value: 2, done: false }
g.next(); // 输出:'Hello',返回:{ value: undefined, done: true }

yield*后接的generator没有return的情况下,yield* gen()可以使用for-of替代,即相当于:

for (let x of gen()) {
    yield x;
}

任何数据结构,只要部署了iterator接口,那么就能够被yield*遍历,如:

function* gen() {
    yield* [1, 2, 3];
}
const g = gen();
g.next(); // {value: 1, done: false}
g.next(); // {value: 2, done: false}
g.next(); // {value: 3, done: false}
g.next(); // {value: undefined, done: true }


九、作为对象属性的generator函数

如果一个对象的属性是generator函数,那么可以简写如:

let obj = {
    * genMethod() {
        // ...
    }
}

这种方式等价于:

let obj = {
    genMethod: function* () {
        // ....
    }
}


十、this的问题

generator函数的作用是返回一个遍历器对象,ES6规定该遍历器对象是generator函数的实例,也继承了generator函数的prototype对象上的方法,如:

function* gen() {}
gen.prototype.hello = function() {
    console.log('Hello');
}
let obj = gen();
obj.hello(); // 输出:Hello

但是,如果把generator函数当做构造函数使用的话,是不会生效的,如:

function* gen() {
    this.a = 1;
}
let obj = gen();
obj.a; // undefined

这是因为:generator函数返回的永远是遍历器对象,而不是this对象,同样的,generator函数不能和new一起使用,即以下形式会报错:

function* F() {
    yield this.x = 2;
    yield this.y = 3;
}
new F(); // TypeError: F is not a constructor

如果希望既能够得到遍历器对象,又能够正常的使用this的行为,那么可以这么做:

function* gen() {
    yield this.a = 1;
    yield this.b = 2;
    yield this.c = 3;
}
let obj = {};
let g = gen.call(obj);
[...g]; // [1, 2, 3];
obj.a; // 1
obj.b; // 2
obj.c; // 3

我们还可以借助实例可以访问generator函数的原型对象上的属性和方法这一特征,实现这个行为如:

function* gen() {
    yield this.a = 1;
    yield this.b = 2;
    yield this.c = 3;
}
const g = gen.call(gen.prototype);
[...g]; // [1, 2, 3];
g.a; // 1
g.b; // 2


十一、Generator与状态机

generator可以很方便地实现状态机,传统方式实现一个状态机,如:

let ticking = true;
const clock = function() {
    if (ticking) {
        console.log('Tick!');
    } else {
        console.log('Tock!');
    }
    ticking = !ticking;
}

这种方式的缺点是比较麻烦,而且还需要用一个外部变量来记录状态。用generator可以改写如下:

function* clock() {
    while (true) {
        console.log('Tick!');
        yield;
        console.log('Tock!');
        yield;
    }
}