Generator知識點雜燴

挖坑埋神經病發表於2018-07-17

Generator

Generator 函式有多種理解角度。語法上,首先可以把它理解成,Generator 函式是一個狀態機,封裝了多個內部狀態。

執行 Generator 函式會返回一個遍歷器物件,也就是說,Generator 函式除了狀態機,還是一個遍歷器物件生成函式。返回的遍歷器物件,可以依次遍歷 Generator 函式內部的每一個狀態。

  • Generator 函式有兩個特徵:

    • function關鍵字與函式名之間有一個星號
    • 函式體內部使用yield表示式,定義不同的內部狀態(yield在英語裡的意思就是“產出”)
      function* helloWorldGenerator() {
      yield 'hello';
      yield 'world';
      return 'ending';
      }
    
      var hw = helloWorldGenerator(); 
      hw.next()
      // { value: 'hello', done: false } value屬性就是當前yield表示式的值,done屬性為false,表示遍歷還沒有結束。
    
      hw.next()
      // { value: 'world', done: false }
    
      hw.next()
      // { value: 'ending', done: true }
    
      hw.next()
      // { value: undefined, done: true } done屬性為true,表示遍歷已經結束。   
    複製程式碼

上面程式碼定義了一個 Generator 函式helloWorldGenerator,它內部有兩個yield表示式(helloworld),即該函式有三個狀態:hello,world 和 return 語句(結束執行)。
呼叫方法與普通函式一樣,但是呼叫,函式並不執行,返回一個指向內部狀態的指標物件,也就是遍歷器物件。 必須呼叫遍歷器物件的next方法,使得指標移向下一個狀態。內部指標就從函式頭部或上一次停下來的地方開始執行,直到遇到下一個yield表示式(或return語句)為止。
換言之,Generator 函式是分段執行的,yield表示式是暫停執行的標記,而next方法可以恢復執行。

yield 表示式

由於 Generator 函式返回的遍歷器物件,只有呼叫next方法才會遍歷下一個內部狀態,所以其實提供了一種可以暫停執行的函式。yield表示式就是暫停標誌。因此等於為 JavaScript 提供了手動的“惰性求值”(Lazy Evaluation)的語法功能。
yield表示式只能用在 Generator 函式裡面,用在其他地方都會報錯。

next 方法的引數

yield表示式本身沒有返回值,或者說總是返回undefinednext方法可以帶一個引數,該引數就會被當作上一個yield表示式的返回值。

    function* foo(x) {
    var y = 2 * (yield (x + 1));
    var z = yield (y / 3);
    return (x + y + z);
    }

    var a = foo(5);
    a.next() // Object{value:6, done:false}
    a.next() // Object{value:NaN, done:false}
    a.next() // Object{value:NaN, done:true}

    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方法的時候不帶引數,導致 y 的值等於2 * undefined(即NaN),除以 3 以後還是NaN,因此返回物件的value屬性也等於NaN。第三次執行Next方法的時候不帶引數,所以z等於undefined,返回物件的value屬性等於5 + NaN + undefined,即NaN

如果向next方法提供引數,返回結果就完全不一樣了。上面程式碼第一次呼叫b的next方法時,返回x+1的值6;第二次呼叫next方法,將上一次yield表示式的值設為12,因此y等於24,返回y / 3的值8;第三次呼叫next方法,將上一次yield表示式的值設為13,因此z等於13,這時x等於5y等於24,所以return語句的值等於42

注意,由於next方法的參數列示上一個yield表示式的返回值,所以在第一次使用next方法時,傳遞引數是無效的。V8 引擎直接忽略第一次使用next方法時的引數,只有從第二次使用next方法開始,引數才是有效的。從語義上講,第一個next方法用來啟動遍歷器物件,所以不用帶有引數。

next方法的引數,也可以向Generator 函式內部輸入值

    function* dataConsumer() {
    console.log('Started');
    console.log(`1. ${yield}`);
    console.log(`2. ${yield}`);
    return 'result';
    }

    let genObj = dataConsumer();
    genObj.next();
    // Started
    genObj.next('a')
    // 1. a
    genObj.next('b')
    // 2. b  
複製程式碼

for...of 迴圈

for...of迴圈可以自動遍歷 Generator 函式時生成的Iterator物件,且此時不再需要呼叫next方法。

    function* foo() {
    yield 1;
    yield 2;
    yield 3;
    yield 4;
    yield 5;
    return 6; //return語句返回的,不包括在for...of迴圈之中
    }

    for (let v of foo()) {
    console.log(v);
    }
    // 1 2 3 4 5  
複製程式碼

除了for...of迴圈以外,擴充套件運算子(...)、解構賦值和Array.from方法內部呼叫的,都是遍歷器介面。這意味著,它們都可以將 Generator 函式返回的 Iterator 物件,作為引數。

    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  
複製程式碼

Generator.prototype.throw()

Generator 函式返回的遍歷器物件,都有一個throw方法,可以在函式體外丟擲錯誤,然後在 Generator 函式體內捕獲。

    var g = function* () {
    try {
        yield;
    } catch (e) {
        console.log('內部捕獲', e);
    }
    };

    var i = g();
    i.next();

    try {
    i.throw('a');
    i.throw('b');
    } catch (e) {
    console.log('外部捕獲', e);
    }
    // 內部捕獲 a
    // 外部捕獲 b  
複製程式碼

一旦執行了catch,捕捉了錯誤,Generator 函式就已經結束了,不再執行下去了。

Generator.prototype.return()

Generator 函式返回的遍歷器物件,還有一個return方法,可以返回給定的值,並且終結遍歷 Generator 函式。

next()、throw()、return() 的共同點

本質上是同一件事,可以放在一起理解。它們的作用都是讓 Generator 函式恢復執行,並且使用不同的語句替換yield表示式。
next()是將yield表示式替換成一個值。

    const g = function* (x, y) {
    let result = yield x + y;
    return result;
    };

    const gen = g(1, 2);
    gen.next(); // Object {value: 3, done: false}

    gen.next(1); // Object {value: 1, done: true}
    // 相當於將 let result = yield x + y
    // 替換成 let result = 1;  
複製程式碼

throw()是將yield表示式替換成一個throw語句。

    gen.throw(new Error('出錯了')); // Uncaught Error: 出錯了
    // 相當於將 let result = yield x + y
    // 替換成 let result = throw(new Error('出錯了'));  
複製程式碼

return()是將yield表示式替換成一個return語句。

    gen.return(2); // Object {value: 2, done: true}
    // 相當於將 let result = yield x + y
    // 替換成 let result = return 2;  
複製程式碼

yield* 表示式

如果在 Generator 函式內部,呼叫另一個 Generator 函式,預設情況下是沒有效果的。yield*表示式,用來在一個 Generator 函式裡面執行另一個 Generator 函式。

    function* foo() {
    yield 'a';
    yield 'b';
    }
    //普通方法呼叫foo() ==========================
    function* bar() {
    yield 'x';
    foo(); 
    yield 'y';
    }
    for (let v of bar()){
    console.log(v);
    }
    // "x"
    // "y"  
    //上面foo()的呼叫是沒有效果的

    //yield*表示式呼叫 =================================
    function* bar() {
    yield 'x';
    yield* foo();
    yield 'y';
    }
    // 等同於
    function* bar() {
    yield 'x';
    yield 'a';
    yield 'b';
    yield 'y';
    }

    // 等同於
    function* bar() {
    yield 'x';
    for (let v of foo()) {
        yield v;
    }
    yield 'y';
    }

    for (let v of bar()){
    console.log(v);
    }
    // "x"
    // "a"
    // "b"
    // "y"
複製程式碼

從語法角度看,如果yield表示式後面跟的是一個遍歷器物件,需要在yield表示式後面加上星號,表明它返回的是一個遍歷器物件。這被稱為yield*表示式。

yield*後面的 Generator 函式(沒有return語句時),等同於在 Generator 函式內部,部署一個for...of迴圈。

    function* concat(iter1, iter2) {
    yield* iter1;
    yield* iter2;
    }

    // 等同於

    function* concat(iter1, iter2) {
    for (var value of iter1) {
        yield value;
    }
    for (var value of iter2) {
        yield value;
    }
    }  
複製程式碼

上面程式碼說明,yield*後面的 Generator 函式(沒有return語句時),不過是for...of的一種簡寫形式,完全可以用後者替代前者。反之,在有return語句時,則需要用var value = yield* iterator的形式獲取return語句的值。

實際上,任何資料結構只要有 Iterator 介面,就可以被yield*遍歷。

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 
複製程式碼

Generator 函式的this

Generator 函式g返回的遍歷器obj,是g的例項,而且繼承了g.prototype。但是,如果把g當作普通的建構函式,並不會生效,因為g返回的總是遍歷器物件,而不是this物件,也不能跟new命令一起用,會報錯。

下面是一個變通方法。首先,生成一個空物件,使用call方法繫結 Generator 函式內部的this。這樣,建構函式呼叫以後,這個空物件就是 Generator 函式的例項物件了。

    function* F() {
    this.a = 1;
    yield this.b = 2;
    yield this.c = 3;
    }
    var obj = {};
    var f = F.call(obj);

    f.next();  // Object {value: 2, done: false}
    f.next();  // Object {value: 3, done: false}
    f.next();  // Object {value: undefined, done: true}

    obj.a // 1
    obj.b // 2
    obj.c // 3  
複製程式碼

還有一個辦法就是將obj換成F.prototype

    function* F() {
    this.a = 1;
    yield this.b = 2;
    yield this.c = 3;
    }
    var f = F.call(F.prototype);

    f.next();  // Object {value: 2, done: false}
    f.next();  // Object {value: 3, done: false}
    f.next();  // Object {value: undefined, done: true}

    f.a // 1
    f.b // 2
    f.c // 3  
複製程式碼

再將F改成建構函式,就可以對它執行new命令了。

    function* gen() {
    this.a = 1;
    yield this.b = 2;
    yield this.c = 3;
    }

    function F() {
    return gen.call(gen.prototype);
    }

    var f = new F();

    f.next();  // Object {value: 2, done: false}
    f.next();  // Object {value: 3, done: false}
    f.next();  // Object {value: undefined, done: true}

    f.a // 1
    f.b // 2
    f.c // 3
複製程式碼