ES6 Generator函数之基本用法(1)

Generator函数之基本用法

(1)基本概念

Generator函数是ES6 提供的一种异步编程解决方案,语法与传统函数完全不同。

Generator函数与普通函数在写法上的不同

1.function命令与函数名之间有一个星号(*)。
2.函数体内部使用yield语句定义不同的内部状态。

Generator函数的调用方法

Generator函数的调用方法与普通函数一样,也是在函数名后面加上一个圆括号。但是,调用Generator函数后,这个函数并不会执行。返回的是一个指向内部状态的指针对象,也就是遍历器对象。
接下来必须调用遍历器对象的next方法,使得指针移向下一个状态。每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一条yield语句(或return语句为止)。换言之,Generator函数是分段执行的,yield语句是暂停执行的标记,而next方法可以恢复执行。

   function* generator() {
        yield "q";
        yield "w";
        return "e";
    }

    let a = generator();
    console.log(a.next());
    //{value:"q",done:false}
    console.log(a.next());
    //{value:"w",done:false}
    console.log(a.next());
    //{value:"e",done:true}
    console.log(a.next());
    //{value:undefined,done:true}

1.调用Generator函数返回一个遍历器对象,代表Generator函数的内部指针。以后每次调用遍历器对象的next方法,就会返回一个有着value和done两个属性的对象。

value表示当前的内部状态的值,是yield语句后面的表达式的值(如果yield后面没有值,value属性的值就是undefined);done属性是一个布尔值,表示是否遍历结束。

2.如果遇到return语句,则value属性的值是return语句后面的表达式的值(如果return语句后面没有值,value属性的值就是undefined),done属性的值为true,表示遍历结束。

3.如果done属性变为true后继续调用next方法,返回的value属性的值为undefined,done属性的值为true。

(2)yield表达式

Generator函数返回的遍历器对象,只有调用next方法才会遍历下一个内部状态,所以说Generator函数是一种可以暂停执行的函数。yield就是暂停执行的标志。

1.遍历器对象的next方法的运行逻辑:

1.遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。
2.下一次调用next方法时,再继续往下执行,直到遇到下一个yield表达式。
3.如果没有再遇到新的yield表达式,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值。
4.如果该函数没有return语句,则返回的对象的value属性值为undefined。

2.惰性求值

只有调用next方法且内部指针指向该语句时才会执行yield语句后面的表达式。

    function* generator() {
        yield 1 + 2;
    }

上面的代码中,yield后面的表达式1+2不会立即求值,只会在next方法将指针移到这一句时才会求值。

3.yield语句与return语句

yield表达式与return语句既有相似之处,也有区别。相似之处在于,都能返回紧跟在语句后面的那个表达式的值。区别在于每次遇到yield,函数暂停执行,下一次再从该位置继续向后执行,而return语句不具备位置记忆的功能。一个函数里面,只能执行一次(或者说一个)return语句,但是可以执行多次(或者说多个)yield表达式。正常函数只能返回一个值,因为只能执行一次return;Generator 函数可以返回一系列的值,因为可以有任意多个yield。

4.Generator 函数可以不用yield表达式,这时就变成了一个单纯的暂缓执行函数
function* f() {
  console.log('执行了!')
}

var generator = f();

setTimeout(function () {
  generator.next()
}, 2000);

上面代码中,函数f如果是普通函数,在为变量generator赋值时就会执行。但是,函数f是一个 Generator 函数,就变成只有调用next方法时,函数f才会执行。

5.yield表达式只能用在 Generator 函数里面

yield表达式如果用在其他函数中就会报错。

var 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;
    }
  });
};

for (var f of flat(arr)){
  console.log(f);
}

上面的代码中,forEach方法的参数是一个普通函数,但是在里面使用了yield表达式(也使用了yield*表达式),所以会产生句法错误。

6.yield表达式如果用在另一个表达式之中必须放在圆括号中
 function* generator() {
        console.log("ni hao " + (yield 123));
        console.log("hello " + (yield 456))
    }

    let a = generator();
    
    console.log(a.next());
    //{value:123 done:false}
    console.log(a.next());
    // ni hao undefined
    //{value:456 done:false}
    console.log(a.next());
    // hello undefined
    //{value:undefined done:true}
    console.log(a.next());
    //{value:undefined done:true}

yield表达式用作函数的参数或者放在赋值表达式的右边,可以不加括号。

(3)与Iterator接口的关系

任意一个对象的Symnol.iterator方法等于该对象的遍历器对象生成函数,调用该函数会返回该对象的遍历器对象。
由于Generator函数就是遍历器生成函数,因此可以把Generator赋值给对象的Symbol.iterator属性,从而使得该对象具有Iterator接口。

    let demo = {};
    demo[Symbol.iterator] = function* () {
        yield 1;
        yield 2;
        yield 3;
    };
    let a=[...demo];
    console.log(a);
    //[1,2,3]
    
    for(let item of demo){
        console.log(item);
    }
    //1
    //2
    //3

Generator 函数执行后,返回一个遍历器对象。该对象本身也具有Symbol.iterator属性,执行后返回自身。

function* gen(){
  // some code
}

var g = gen();

g[Symbol.iterator]() === g
// true

上面代码中,gen是一个 Generator 函数,调用它会生成一个遍历器对象g。它的Symbol.iterator属性,也是一个遍历器对象生成函数,执行后返回它自己。

(4)next方法的参数

yield语句本身没有返回值。next方法可以带一个参数,这个参数会被当作上
一条yield语句的返回值。

  function* generator() {
        let b=yield "q";
        let a=b/2;
        console.log(a);
        yield "w";
        return "e";
    }

    let a = generator();
    console.log(a.next());
    console.log(a.next(2))
    //{value:q,done:false}
    //1
    //{value:w,done:false}

第二次调用next方法的时候,传入了参数2,因为next方法传入的参数被当作上一条yield语句的返回值,所以这是b的值为2,之后a=b/2,a的值为1,之后打印出1。

通过next方法的参数,可以在Generator函数运行的不同阶段从外部向内部注入不同的值,从而可以调整函数的行为。
注意第一次使用next方法时传递参数是无效的:因为next方法的参数表示上一条yield语句的返回值,所以第一次使用next方法时传递的参数是无效的。

(5)for…of循环

for…of循环可以自动遍历Generator函数返回的遍历器对象,此时不再需要调用next方法:

  function* generator() {
        yield "q";
        yield "w";
        return "e";
    }
    for(let item of generator()){
        console.log(item)
    }
    //q
    //w

一旦next方法的返回对象的done属性为true,for…of循环就会终止,且不包含该返回对象,所以上面的return语句返回的"e"不包括在for…of循环中。
原生的普通对象没有遍历接口,无法使用for…of循环,通过Generator函数可以为它加上这个接口:

function* objectEntries(obj) {
  let propKeys = Reflect.ownKeys(obj);

  for (let propKey of propKeys) {
    yield [propKey, obj[propKey]];
  }
}

let jane = { first: 'Jane', last: 'Doe' };

for (let [key, value] of objectEntries(jane)) {
  console.log(`${key}: ${value}`);
}
// first: Jane
// last: Doe

另外一种写法:

function* objectEntries() {
  let propKeys = Object.keys(this);

  for (let propKey of propKeys) {
    yield [propKey, this[propKey]];
  }
}

let jane = { first: 'Jane', last: 'Doe' };

jane[Symbol.iterator] = objectEntries;

for (let [key, value] of jane) {
  console.log(`${key}: ${value}`);
}
// first: Jane
// last: Doe

除了for…of循环以外,扩展运算符(…)、解构赋值和Array.from方法内部调用的,都是遍历器接口。这意味着,它们都可以将 Generator 函数返回的遍历器对象,作为参数。

function* numbers () {
  yield 1
  yield 2
  return 3
  yield 4
}

// 扩展运算符
[...numbers()] // [1, 2]

// Array.from 方法
Array.from(numbers()) // [1, 2]

// 解构赋值
let [x, y] = numbers();
x // 1
y // 2

// for...of 循环
for (let n of numbers()) {
  console.log(n)
}
// 1
// 2

(6)Generator.prototype.throw()

1.Generator函数返回的遍历器对象都有一个throw方法

可以在函数体外抛出错误,然后在Generator函数体内捕获。同时throw方法可以接受一个参数,这个参数会被catch语句接受。

   function* generator() {
        try {
            yield;
        } catch (e) {
            console.log("内部捕获 " + e);
        }
    }

    let i = generator();
    i.next();
    i.throw("a");
    //内部捕获 a

如果遍历器对象抛出两个错误,而Generator函数体内只有一个try/catch语句,则第二次抛出的错误不会被捕获:

 function* generator() {
        try {
            yield;
        } catch (e) {
            console.log("内部捕获 " + e);
        }
    }

    let i = generator();
    i.next();
    i.throw("a");
    i.throw("b");
    //内部捕获 a
    //Uncaught b

遍历器对象i抛出第一个错误,被Generator函数内部的catch语句捕获并处理,这是catch语句已经执行过了。之后i抛出第二个错误,但是因为Generator函数内部的catch语句已经执行过了,所以不会捕捉到这个错误。

如果遍历器对象有两个try/catch语句:

    function* generator() {
        try {
            yield;
        } catch (e) {
            console.log("内部捕获1 " + e);
        }
        try {
            yield;
        } catch (e) {
            console.log("内部捕获2 " + e);
        }
    }

    let i = generator();
    i.next();
    i.throw("a");
    i.throw("b");
    //内部捕获1 a
    //内部捕获2 b
2.注意遍历器对象的throw方法与全局throw命令的不同

用遍历器对象的throw方法抛出的错误才会被Generator函数内部的catch语句捕获。而全局的throw命令抛出的错误只能被Genenrator函数体外的catch语句捕获。

    function* generator() {
        try {
            yield;
        } catch (e) {
            console.log("内部捕获 " + e);
        }
    }

    let i = generator();
    i.next();
    throw("a");
    //Uncaught a
   function* generator() {
        try {
            yield;
        } catch (e) {
            console.log("内部捕获 " + e);
        }
    }

    let i = generator();
    i.next();
    i.throw("a");
    //内部捕获 a
    try{
        throw("b");
    }catch(e){
        console.log("外部捕获 "+e)
    }
    //外部捕获 b 

函数外部的一个try/catch语句也是一次只能捕获一个错误:

    try{
        throw("a");
        throw("b");
    }catch(e){
        console.log("外部捕获 "+e)
    }
    //外部捕获 a
3.如果Generator函数内部没有部署try/catch语句,则throw语句抛出的错误将被外部的try/catch语句捕获
  function* generator() {
        yield;
    }

    let i = generator();
    i.next();
    try {
        i.throw("a");
    } catch (e) {
        console.log("外部捕获 " + e)
    }
    //外部捕获 a
4.throw方法对下一次遍历的影响
如果Generator函数没有部署try/catch语句,则调用throw方法会使遍历终止
    function* generator() {
        yield 123;
        yield 456;
        yield 789;

    }

    let i = generator();
    console.log(i.next());
    //{value:123 done:false}
    i.throw("a");
    //Uncaught a
如果Generator函数部署了try/catch语句,则调用throw方法不影响下一次遍历

throw方法被捕获之后会附带执行下一条yield表达式,也就是会附带执行一次next方法:

    function* generator() {
        try{
            yield 123;
        }catch (e) {
            console.log("内部捕获 "+e)
        }

        yield 456;
        yield 789;

    }

    let i = generator();

    console.log(i.next());
    //{value:123 done:false}
    console.log(i.throw("a"));
    //内部捕获 a
    //{value:456 done:false}
    console.log(i.next());
    //{value:789 done:false}
    console.log(i.next());
    //{value:undefined done:true}

(7)Generator.prototype.return()

Generator函数返回的遍历器对象还有一个return方法,可以返回给定的值,并终结Generator函数的遍历。

1.return方法带参数,返回值的value属性为参数值
   function* generator() {
        yield 123;
        yield 456;
        yield 789;

    }

    let i = generator();

    console.log(i.next());
    //{value:123 done:false}
    console.log(i.return("a"));
    //{value:"a" done:true}
    console.log(i.next());
    //{value:undefined done:true}

遍历器对象调用return方法后,返回值的value属性就是return方法的参数(这里是“a”)。同时Generator函数的遍历终止,返回值的done属性为true。以后再调用next方法,value属性的值总为undefined,done属性的值总为true。

2.如果return方法不提供参数,则返回值的value属性为undefined
   function* generator() {
        yield 123;
        yield 456;
        yield 789;

    }

    let i = generator();

    console.log(i.next());
    //{value:123 done:false}
    console.log(i.return());
    //{value:undefined done:true}
    console.log(i.next());
    //{value:undefined done:true}
3.return方法遇到Generator函数内部有try…finally代码块

如果 Generator 函数内部有try…finally代码块,且正在执行try代码块,那么return方法会推迟到finally代码块执行完再执行。

function* numbers () {
  yield 1;
  try {
    yield 2;
    yield 3;
  } finally {
    yield 4;
    yield 5;
  }
  yield 6;
}
var g = numbers();
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 }

上面代码中,调用return方法后,就开始执行finally代码块,然后等到finally代码块执行完,再执行return方法。

如果 Generator 函数内部有try…finally代码块,但是现在并没有执行try代码块,那么return方法会直接执行,返回的值的value属性是return方法的参数,done属性是true。

   function* numbers() {
        yield 1;
        try {
            yield 2;
            yield 3;
        } finally {
            yield 4;
            yield 5;
        }
        yield 6;
    }

    let g = numbers();
    console.log(g.next());
    //{value:1,done:false}
    console.log(g.return(7));
    //{value:7,done:true}
    console.log(g.next());
    //{value:undefined,done:true}

参考文献:《ECMAScript 6 入门》阮一峰

猜你喜欢

转载自blog.csdn.net/weixin_42695446/article/details/84504649