Generator函数的一些笔记

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接口
  • 作为数据结构
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值