一、介绍
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;
}
}