1、Generator基本概念
注:【遍历器】和【迭代器】是一个意思。
1.1、表象:
- 函数名有个星号(就是乘号*);
- 函数体内部使用yield表达式(yield翻译成中文:产生);
- 函数调用后返回值是一个遍历器对象,该对象可以调用next方法遍历yield表达式返回的值;
- 函数内部可有返回值,也可以没有(这个影响的是遍历器遍历到第一次done为true时,value的值是什么);
- 函数执行后并不直接执行函数内部代码,而是返回一个遍历器(参考上一章Iterator),但注意,他本身并没有
[Symbol.iterator]
属性
简单来理解,就是当函数名前面有星号时,调用函数的时候不会运行该函数,而是返回一个遍历器,该遍历器通过调用next方法可以遍历执行函数内部的语句,每次执行到yield表达式(或执行到函数结束)为止;
如示例代码:
function* foo() { // 星号在function关键词和函数名之间
yield 'first'; // yield表达式表示会被遍历到的内容
let second = 'sec'; // 函数内部可以有正常的js语句
yield second + 'ond'; // yield表达式只关心该表达式的返回值(即"second"字符串)
return 'last' // 可以有return返回值也可以没有
}
foo[Symbol.iterator]; // undefined,说明没有这个属性
let bar = foo();
// bar:
// {[[GeneratorStatus]]: "suspended"}
// 原型链上有next、return、throw三个方法
bar.next(); // {value: "first", done: false}
bar.next(); // {value: "second", done: false}
bar.next(); // {value: "last", done: true} 如果没有return,那么这里的value是undefined
bar; // {[[GeneratorStatus]]: "closed"} 出现done之后才会closed
1.2、运行:
函数本身的运行:
- Generator函数可以视为一个状态机;
- 简单来说,就是内部可能有A、B、C状态,初始是空状态,然后通过多次调用next方法,依次切换到A、B、C状态;
- 当之后没有其他状态时,调用next方法返回的对象的done属性的值为true;
这三句话可能比较抽象,可以结合下面代码的执行来理解:
代码的执行:
- 直接调用本函数,并不会执行函数内部的代码,而是返回一个遍历器;
- 此时什么都不执行,直到调用该遍历器的next方法为止;
- 第一次调用next时,从函数内部第一行开始执行,直到遇见yield表达式为止,输出yield表达式的值(即yield所在的那一段js语句的返回值);
- 第二次调用next时,从上一次yield表达式停止的地方开始继续执行,直到再次遇见yield表达式,或者到函数的结尾,或遇见return后停止;
- 注意,是执行到yield表达式为止,而不是yield所在的那一行代码,典型情况是:
console.log(yield "test");
其中的console.log是不执行的; - 每次执行next()后,返回值的是一个对象,有value属性和done属性,参考Iterator,其中value的值是yield表达式的值;
- 当结束到结尾时,或者遇见return时,done会变为true,在此之前,done为false;
- done变为true,没必要继续调用next了(就像Iterator接口那样);
首先,当然是上示例代码啦:
function* foo() {
let a = 1;
console.log('第一次执行')
yield a;
a++;
console.log(yield "test");
a *= 2;
return a
}
// 调用foo()
let bar = foo();
// 此时没有输出任何东西,bar的值
// 第一次调用next(),下面的两行注释,其中第一行是console.log,第二行是返回值****
bar.next();
// 第一次执行
// {value: 1, done: false}
// 第二次调用next(),注意,这个时候没执行yield "test"外面的console.log
bar.next();
// {value: "test", done: false}
// 第三次调用next(),这个时候执行了console.log(),但显然由于yield之前已经执行了,所以参数是空
// 遇见return了,所以done变为true,value的值是return的返回值
bar.next();
// undefined
// {value: 4, done: true}
看注释,就懂代码怎么执行的了。
另外提一句,每次通过foo()生成的遍历器,都是独立的,他们之间不会互相影响。
1.3、yield关键字
首先,yield在非Generator函数内不是关键字,只有在Generator函数内才是关键字,可以通过赋值,然后不会报错来证明。
因此,不要试图在非Generator函数内部的场合使用yield,肯定会报错的啦。
let yield = 1;
console.log(yield); //1
1.4、yield表达式的值
yield表达式起到的是暂停函数执行的作用,该表达式的值是下一次调用next()时作为参数传入的值,默认情况下是undefined。
原因在于,每次执行到yield表达式时就会终止,等到下次执行的时候,相当于yield表达式所在的地方为空(上次执行过了,所以这次不会再执行一遍),所以值undefined。
那么怎么让yield表达式有值呢?答案是通过遍历器的next()方法的参数来传入。该参数将作为上一个yield表达式的值来使用;
function bar(val) {
console.log(val, arguments.length)
}
function* foo() {
console.log('Hello ' + (yield 123));
bar(yield 456)
}
let test = foo();
test.next();
// {value: 123, done: false}
test.next();
// Hello undefined
// {value: 456, done: false}
test.next('input');
// input 1
// {value: undefined, done: true}
另外提一句,由于第一次调用next()时,之前是没有yield表达式的,所以如果想在初始化的时候就输入值,需要进行特殊化处理,比如外面包一层,或者放弃第一次next输入参数,空调用一次next()来实现(这个也是外面包一层的实现原理)
如示例代码(引自阮一峰的):
// 包一层
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!')
// First input: hello!
// {value: "DONE", done: true}
// 空调用一次next()
function* dataConsumer() {
console.log(`1. ${yield}`);
console.log(`2. ${yield}`);
return 'result';
}
let genObj = dataConsumer();
genObj.next();
genObj.next('a')
// 1. a
genObj.next('b')
// 2. b
最后,yield表达式如果单独使用,那么不需要括号,如果要将yield表达式与其他变量进行运算,那么需要使用圆括号将其括起来(如上面的示例);
1.5、Generator的简写
普通函数作为对象属性的时候可以简写,Generator函数作为对象属性的时候,虽然多了一个星号,但也可以简写,简写方法是将星号放在属性名前即可
如代码:
let foo = {
* [Symbol.iterator]() {
// some code
}
}