1. 程式人生 > >ES6 Generator函式之基本用法(1)

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