Generator函数
1.简介
1.1 基本概念
Generator函数从语法上可以理解为一个状态机,封装了多个内部状态。
执行Generator函数会返回一个遍历器对象。也就是说Generator函数除了是一个状态机,还是一个遍历器对象生成函数。返回的遍历器对象可以依次遍历函数内部的每一个状态。
特征:
- function命令与函数名之间有一个*号
- 函数体内部使用yield语句定义不同的内部状态
Generator函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号,不同的是,调用Generator函数后,该函数并不执行,而是返回一个指向内部状态的指针对象(遍历器对象)。每次调用next方法,内部指针就从函数头部或上一次听写爱来的地方开始执行,直到遇到下一条yield语句或return语句为止。返回一个有value和done属性的对象,value属性表示当前内部状态的值,是yield语句后面的那个表达式;done属性是一个布尔值,表示遍历是否结束。
*号的位置,以下都OK
function*foo(x,y){ ... }
function *foo(x,y){ ... }
function * foo(x,y){ ... }
function*foo(x,y){ ... }
1.2 yield表达式
由于Generator函数返回的遍历器对象只有调用next方法才会遍历下一个内部状态,所以提供了一种可以暂停执行的函数。yield语句就是暂停的标志。
遍历器对象next方法的执行逻辑如下:
- 遇到yield语句就暂停执行后面的操作,并将紧跟在yield后的表达式的值作为返回对象的value属性。
- 下一次调用next方法时继续往下执行,直到遇到下一条yield语句
- 如果没有遇到yield语句就一直运行到函数结束,直到return语句为止,并将return语句后面表达式的值作为返回对象的value属性值。
- 如果没有return语句,则返回对象的value属性值为undefined
yield语句与return异同:
- 都能返回紧跟在语句后表达式的值
- 每次遇到yield函数暂停执行,下一次会在该位置继续向后执行,而return语句不具备位置记忆功能。一个函数里面只能执行一次return语句,但是可以执行多次yield语句。
Generator函数可以不用yield语句,那么就变成了一个单纯的暂缓执行函数。
yield表达式只能用在Generator函数里,用在其他地方都会报错。
(
function (){
yield 2;
}
)()//报错
在一个普通函数中使用yield语句会报错。
let 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;
}
}
)
}
上面的代码也会报错,forEach方法的参数是一个普通函数,但是在里面使用了yield,yield*。
如果yield表达式用在另一个表达式之中,必须放在圆括号里。
如果yield表达式用作函数参数或放在赋值表达式的右边,可以不加括号。
1.3与Iterator接口的关系
任意一个对象的Symbol.iterator方法等于该对象的遍历器对象生成函数,调用该函数会返回该对象的一个遍历器对象。因此可以把Generator复制给对象的Symbol.iterator属性,从而使得该对像具有iterator接口。
var myIterator = {};
myIterator[Symbol.iterator] = function* {
yield 1;
yield 2;
yield 3;
}
[...myIterator];//[1 , 2 , 3]
//有了iterator接口接口之后可以用扩展运算符...转换为数组
Generator函数执行之后返回一个遍历器对象。该对象本身也有Symbol.iterator属性,它的Symbol.iterator属性也是一个遍历器对象生成函数,执行后返回它本身。
function* gen(){
//...
}
let g = gen();
g[Symbol.iterator]() === g; //true
2.next()方法的参数
yield语句本身没有返回值,或者说它总是返回undefined。next方法可以带有一个参数,该参数会被当做上一条yield语句的返回值。
function* f(){
for(var i = 0 ; true ; i++){
var reset = yield i;
if(reset){
i = -1;
}
}
}
var g = f();
g.next();// {value: 0 , done: false}
g.next();// {value: 1 , done: false}
g.next(true);// {value: 0 , done: false}
上面的代码定义了一个可以无限运行的Generator函数f,如果next方法没有参数,每次运行到yield语句时,由于yield语句没有返回值(返回undefined),以至于reset的值也是undefined,if后的代码就不会执行。当next方法带有一个参数true时,上一条yield语句的返回值变成true,所以reset的值为true,i的值被置为-1,当再次进入for循环,i又从1开始递增。
Generator函数从暂停状态到恢复运行,其上下文是不变的。通过next方法的参数就有办法在Generator函数开始运行后继续向函数内部注入值。也就是说,可以在enerator函数运行的不同阶段从外部向内部注入不同的值,从而调整函数行为。
下面再看另外一个例子。
function* foo(x){
var y = 2 * ( yield ( x + 1 ));//yield表达式用在另一个表达式之中,必须放在圆括号里
var z = yield (y /3 );//yield表达式用作函数参数或放在赋值表达式的右边,可以不加括号
return ( x + y + z );
}
var a = foo(5);
a.next();//{value: 6 , done:false}
a.next();//{value: NaN , done:false}
a.next();//{value: NaN , done:true}
第二次运行next没有带参数,导致y的值为undefined,2 * undefined 为NaN,除以3还是NaN,于是返回的对象value为NaN。5 + NaN + undefined 返回NaN。
var b = foo(5);
b.next();//{value: 6 , done:false}
b.next(12);//{value: 8 , done:false}
b.next(13);//{value:42 , done:true}
由于next方法的参数表示上一条yield语句的返回值,所以第一次使用next方法时传递参数是无效的。V8引擎直接忽略第一次使用next方法时的参数,只有从第二次使用next方法时,参数才是有效的。语义上,第一个next方法用来启动遍历器对象,所以不用带有参数。
再看一个通过next方法的参数向Generator函数内部输入值的例子。
function* data(){
console.log('started');
console.log(`1.${yield}`);
console.log(`2.${yield}`);
}
let a = data();
console.log(a.next());
//started
console.log(a.next('a'));
//1.a
console.log(a.next('b'));
//2.b
console.log(a.next());
每次通过next方法向Generator函数输入值,然后打印出来。
3.for…of循环
for…of循环可以自动遍历Generator函数生成的Iterator对象,且此时不再需要调用next方法。
function* foo(){
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
return 6;
}
for(let i of foo()){
console.log(i);
}
//1 2 3 4 5
一旦next方法的返回对象的done属性为true,for…of循环就会终止,且不包含该返回的对象,由于return语句返回的对象为{value : 6 , done : true},所以返回的结果不包括在for…of循环里。
- 原生的js对象没有遍历接口,无法使用for…of循环,通过Generator函数为它加上这个接口就可以了。
- 另一种写法是将Generator函数加到对象的Symbol.iterator属性上。
除了for…of循环,扩展运算符(…),解构赋值和Array.from方法内部调用的都是遍历器接口。意味着,Generator函数返回的Iterator对象都可以作为参数。
function* numbers(){
yield 1;
yield 2;
return 3;
yield 4;
}
// 扩展运算符
[...numbers()];// [ 1 , 2 ]
Array.from(numbers());// [ 1 , 2 ]
let [ x , y] = numbers();
x; // 1
y; // 2
for(let n of numbers()){
console.log(n);// 1 2
}
4.Generator.prototype.throw()
- Generator函数返回的遍历器对象都有一个throw方法,可以在函数体外抛出错误,然后在函数体内捕获。
- throw可以接受一个参数,该参数会被catch方法语句接收,建议抛出Error对象的实例。
- 不要混淆遍历器对象的throw方法和全局的throw命令。后者只能被函数体外的catch语句捕获。
- 如果Generator函数内部没有部署try…catch代码块,那么thorw方法抛出的错误将被外部的try…catch代码块捕获。
- 如果Generator函数内部部署t了ry…catch代码块,那么thorw方法抛出的错误不影响下一次遍历,否则遍历直接终止。
- throw方法被捕获以后会附带执行下一条yield表达式,即附带执行下一次next方法。
- 另外,throw命令与throw方法无关,俩者互不干扰。
- 一旦Generator函数执行过程中抛出错误,就不会再执行下去了。js引擎默认为这个Generator函数运行结束。
好处:
这种函数体内捕获错误的机制大大方便了对错误的处理,对于多个yield表达式,可以只用一个try…catch代码块来捕获错误。
5.Generator.prototype.return()
-
Generator函数返回的遍历器对象的return方法可以返回给定的值,并终止Generator函数的遍历。
-
如果调用return方法时不提供参数,则返回值的value属性为undefined。
-
如果Generator函数内部有try…finally代码块,那么return方法会推迟到finally代码块执行完再执行。调用return方法后就开始执行finally代码块,然后等到finally代码块执行完后再执行return方法。
6.yield 表达式*
如果在Generator函数内部调用另一个Generator函数,默认情况下是没有效果的。这时就需要用到 yield* 语句,用来在Generator函数里面执行另一个Generator函数。
function* foo(){
yield '1';
yield '2';
}
function* bar(){
yield 'a';
yield* foo();
yield 'b';
}
//相当于
function* bar(){
yield 'a';
yield '1';
yield '2';
yield 'b';
}
//相当于
function* bar(){
yield 'a';
for(let v of foo()){
yield v;
}
yield 'b';
}
如果yield命令后面跟的是一个遍历器对象,那么需要在yield命令后加一个*,表明返回的是一个遍历器对象。
let delegatedIterator = ( function* (){
yield 'hello';
yield 'bye';
}() ) // 立即执行的匿名函数
let delegatingIterator = (function* (){
yield 'greeting!';
yield* delegatedIterator;
yield 'ok,bye.';
}())
for(let v of delegatingIterator){
console.log(v);
}
// 'greeting!'
// 'hello'
// 'bye'
// 'ok,bye.'
上面的代码中,delegatingIterator是代理者,delegatedIterator是被代理者。
yield* 后面的Generator函数(没有return语句),等同于在Generator函数内部部署一个for…of循环。
任何数据结构只要有Iterator接口,就可以被yield* 遍历。
//数组
function* gen(){
yield* [1 , 2 , 3];
}
gen().next; // {value : 1,done : false}
//字符串
let read = (function* (){
yield 'hello';
yield* 'hello';
})()
read.next().value; //'hello'
read.next().value; // 'h'
如果被代理的Generator函数有return语句,则需要用var value = yield* iterator 的形式获取return语句的值,所以便可以向代理它的Generator函数返回数据。
function* foo() {
yield 2;
yield 3;
return 'foo';
}
function* bar() {
yield 1;
var v = yield* foo();
console.log('v :' + v);
yield 4;
}
let it = bar();
it.next(); // {value : 1 , done : false}
it.next(); // {value : 2 , done : false}
it.next(); // {value : 3 , done : false}
it.next();
//v :foo
// {value : 4 , done : false}
it.next();
// {value : undefined , done : true}
yield* 命令可以很方便的取出嵌套数组的所有成员。
function* iterTree(tree){
//如果当前传入是数组
if(Array.isArray(tree)){
for(let i = 0 ; i < tree.length ;i++){
// 继续对每个元素进行判断
yield* iterTree(tree[i]);
}
}else{
//如果不是数组 直接返回
yield tree;
}
}
const tree = ['a', ['b' , 'c'] ,['d' , 'e'];
for(let x of iterTree(tree)){
console.log(x);
}
// a b c d e
7.作为对象属性的Generator函数
如果一个对象的属性是Generator函数,那么可以写成:
let obj = {
a: function* (){
//...
}
}
//简写
let obj = {
* a (){
//...
}
}
8.Generator函数的this
Generator函数总是返回一个遍历器对象,ES6规定这个遍历器对象是Generator函数的实例,它也继承了Generator函数的prototype对象上的方法。
但是,如果把Generator函数当做普通的函数,则并不会生效。因为Generator函数总是返回遍历器对象,而不是this对象。
Generator函数也不能跟new命令一起用,否则会报错。
解决:将Generator函数内部的this绑定到Generator函数的prototype上。再将Generator函数改成构造函数。
function* gen(){
this.a = 1;
yield this.b = 2;
yield this.c = 3;
}
function F(){
return gen.call(gen.prototype);//将gen函数的this绑定到gen.prototype上
}
var f = new F();// F是构造函数 可以new
f.next(); //{value : 2 , done : false}
// ...
f.a // 1
//...
9.含义
9.1 Generator与状态机
Generator是实现状态机的最佳结构,无需保存状态的外部变量,更简洁,更安全。
9.2 Generator与协程
10.应用
- 异步操作的同步化表达
- 控制流管理
- 部署Iterator接口
- 作为数据结构