ES6經常使用但被忽略的方法(第六彈Generator )

寫在開頭

  • ES6經常使用但被忽略的方法 系列文章,整理做者認爲一些平常開發可能會用到的一些方法、使用技巧和一些應用場景,細節深刻請查看相關內容鏈接,歡迎補充交流。

相關文章

Generator

基本概念

  • Generator 函數是 ES6 提供的一種異步編程解決方案,語法行爲與傳統函數徹底不一樣。語法上,Generator 函數是一個狀態機,封裝了多個內部狀態。執行 Generator 函數會返回一個遍歷器對象,能夠依次遍歷 Generator 函數內部的每個狀態。
  1. 特徵
    1. function關鍵字與函數名之間有一個星號;
    2. 函數體內部使用yield表達式,定義不一樣的內部狀態。
    function* detanxGenerator() {
      yield 'detanx';
      return 'ending';
    }
    
    const dg = detanxGenerator();
    複製代碼
  2. 調用
    dg.next() // { value: 'detanx', done: false }
    dg.next() // { value: 'ending', done: true }
    dg.next() // { value: undefined, done: true }
    複製代碼
    1. 第一次調用,Generator 函數開始執行,直到遇到第一個yield表達式爲止。next方法返回一個對象,它的value屬性就是當前yield表達式的值hellodone屬性的值false,表示遍歷尚未結束。
    2. 第二次調用,Generator 函數從上次yield表達式停下的地方,一直執行到return語句(若是沒有return語句,就執行到函數結束)。done屬性的值true,表示遍歷已經結束。
    3. 第三次調用,此時 Generator 函數已經運行完畢,next方法返回對象的value屬性爲undefineddone屬性爲true。之後再調用next方法,返回的都是這個值。
  3. 寫法
    • function關鍵字與函數名之間的*未規定,因此有不少寫法,咱們寫得時候最好仍是使用第一種,即*緊跟着function關鍵字後面,*後面再加一個空格。
    function* foo(x, y) { ··· }
    function*foo(x, y) { ··· }
    function *foo(x, y) { ··· }
    function * foo(x, y) { ··· }
    複製代碼

yield 表達式

  • Generator 函數返回的遍歷器對象,只有調用next方法纔會遍歷下一個內部狀態,因此其實提供了一種能夠暫停執行的函數。yield表達式就是暫停標誌。
  1. next方法的運行邏輯
    (1)遇到yield表達式,就暫停執行後面的操做,並將緊跟在yield後面的那個表達式的值,做爲返回的對象的value屬性值。
    (2)下一次調用next方法時,再繼續往下執行,直到遇到下一個yield表達式。
    (3)若是沒有再遇到新的yield表達式,就一直運行到函數結束,直到return語句爲止,並將return語句後面的表達式的值,做爲返回的對象的value屬性值。
    (4)若是該函數沒有return語句,則返回的對象的value屬性值爲undefined
  2. yield表達式後面的表達式,只有當調用next方法、內部指針指向該語句時纔會執行。下面的123 + 456不會當即求值,只有當執行next到對應的yield表達式纔會求值。
function* gen() {
  yield  123 + 456;
}
複製代碼
  1. yield表達式只能用在 Generator 函數裏面,用在其餘地方都會報錯。
(function (){
  yield 1;
})()
// SyntaxError: Unexpected number
複製代碼
  1. yield表達式若是用在另外一個表達式之中,必須放在圓括號裏面。用做函數參數或放在賦值表達式的右邊,能夠不加括號。
function* demo() {
  console.log('Hello' + yield); // SyntaxError
  console.log('Hello' + yield 123); // SyntaxError

  console.log('Hello' + (yield)); // OK
  console.log('Hello' + (yield 123)); // OK
}
// 參數和表達式右邊
function* demo() {
  foo(yield 'a', yield 'b'); // OK
  let input = yield; // OK
}
複製代碼

與Iterator接口和for...of 循環的關係

  1. Iterator 接口的關係
  • 上一彈ES6經常使用但被忽略的方法(第五彈Promise和Iterator)說過,任意一個對象的Symbol.iterator方法,等於該對象的遍歷器生成函數,調用該函數會返回該對象的一個遍歷器對象。
  • Generator 函數就是遍歷器生成函數,能夠把 Generator 賦值給對象的Symbol.iterator屬性,從而使得該對象具備 Iterator 接口。
var myIterable = {};
myIterable[Symbol.iterator] = function* () {
  yield 1;
  yield 2;
  yield 3;
};
[...myIterable] // [1, 2, 3]
複製代碼
  • Generator 函數執行後,返回一個遍歷器對象。該對象自己也具備Symbol.iterator屬性,執行後返回自身。
function* gen(){
  // some code
}
var g = gen();
g[Symbol.iterator]() === g
// true
複製代碼
  1. for...of 循環
  • for...of循環能夠自動遍歷 Generator 函數運行時生成的Iterator對象,且此時再也不須要調用next方法。 一旦next方法的返回對象的done屬性爲truefor...of循環就會停止,且不包含該返回對象。
function* numbers() {
  yield 1;
  yield 2;
  return 3;
}

for (let v of numbers()) {
  console.log(v);
}
// 1 2
複製代碼
  1. 除了for...of循環
  • 擴展運算符(...)、解構賦值和Array.from方法內部調用的,都是遍歷器接口。它們均可以將 Generator 函數返回的 Iterator 對象,做爲參數。
// 擴展運算符
[...numbers()] // [1, 2]

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

// 解構賦值
let [x, y] = numbers();
x // 1
y // 2
複製代碼

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,即NaNnode

  • 若是向next方法提供參數,返回結果就徹底不同了。上面代碼第一次調用b的next方法時,返回x+1的值6;第二次調用next方法,將上一次yield表達式的值設爲12,所以y等於24,返回y / 3的值8;第三次調用next方法,將上一次yield表達式的值設爲13,所以z等於13,這時x等於5y等於24,因此return語句的值等於42es6

  • 因爲next方法的參數表示上一個yield表達式的返回值,因此在第一次使用next方法時,傳遞參數是無效的。算法

next()、throw()、return()

共同點
  • 它們的做用都是讓 Generator 函數恢復執行,而且使用不一樣的語句替換yield表達式。
  1. 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;
複製代碼
  1. throw()是將yield表達式替換成一個throw語句。
gen.throw(new Error('出錯了')); // Uncaught Error: 出錯了
// 至關於將 let result = yield x + y
// 替換成 let result = throw(new Error('出錯了'));
複製代碼
  1. return()是將yield表達式替換成一個return語句。
gen.return(2); // Object {value: 2, done: true}
// 至關於將 let result = yield x + y
// 替換成 let result = return 2;
複製代碼
不一樣點
  1. Generator.prototype.throw()
    • Generator 函數返回的遍歷器對象,throw方法能夠在函數體外拋出錯誤,而後在 Generator 函數體內捕獲。throw方法能夠接受一個參數,該參數會被catch語句接收,建議拋出Error對象的實例。
    var g = function* () {
      try {
        yield;
      } catch (e) {
        console.log(e);
      }
    };
    
    var i = g();
    i.next();
    i.throw(new Error('出錯了!'));
    // Error: 出錯了!(…)
    複製代碼
    • 不要混淆遍歷器對象的throw方法和全局的throw命令。上面代碼的錯誤,是用遍歷器對象的throw方法拋出的,而不是用throw命令拋出的。後者只能被函數體外的catch語句捕獲。
    • 若是 Generator 函數內部沒有部署try...catch代碼塊,那麼throw方法拋出的錯誤,將被外部try...catch代碼塊捕獲。
    var g = function* () {
      while (true) {
        yield;
        console.log('內部捕獲', e);
      }
    };
    
    var i = g();
    i.next();
    
    try {
      i.throw('a');
      i.throw('b');
    } catch (e) {
      console.log('外部捕獲', e);
    }
    // 外部捕獲 a
    複製代碼
    • 若是 Generator 函數內部和外部,都沒有部署try...catch代碼塊,那麼程序將報錯,直接中斷執行。
    var gen = function* gen(){
      yield console.log('hello');
      yield console.log('world');
    }
    
    var g = gen();
    g.next();
    g.throw();
    // hello
    // Uncaught undefined
    複製代碼
    • throw方法拋出的錯誤要被內部捕獲,前提是必須至少執行過一次next方法。 g.throw(1)執行時,next方法一次都沒有執行過。這時,拋出的錯誤不會被內部捕獲,而是直接在外部拋出,致使程序出錯。
    function* gen() {
      try {
        yield 1;
      } catch (e) {
        console.log('內部捕獲');
      }
    }
    
    var g = gen();
    g.throw(1);
    // Uncaught 1
    複製代碼
    • Generator 函數體外拋出的錯誤,能夠在函數體內捕獲;反過來,Generator 函數體內拋出的錯誤,也能夠被函數體外的catch捕獲。
    function* foo() {
      var x = yield 3;
      var y = x.toUpperCase();
      yield y;
    }
    var it = foo();
    it.next(); // { value:3, done:false }
    try {
      it.next(42);
    } catch (err) {
      console.log(err);
    }
    複製代碼
    • 數值是沒有toUpperCase方法的,因此會拋出一個 TypeError 錯誤,被函數體外的catch捕獲。
  2. Generator.prototype.return()
    • Generator 函數返回的遍歷器對象,return方法能夠返回給定的值,而且終結遍歷 Generator 函數。return方法調用時,不提供參數,則返回值的value屬性爲undefined
    function* gen() {
      yield 1;
      yield 2;
      yield 3;
    }
    
    var g = gen();
    
    g.next()        // { value: 1, done: false }
    g.return() // { value: undefined, done: true }
    複製代碼
    • 若是 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 }
    複製代碼

yield* 表達式

  • yield*表達式用來在一個 Generator 函數裏面執行另外一個 Generator 函數。
function* foo() {
  yield 'a';
  yield 'b';
}
function* bar() {
  yield 'x';
  yield* foo();
  yield 'y';
}
for (let v of bar()){
  console.log(v);
}
// "x"
// "a"
// "b"
// "y"
複製代碼
  • 若是yield*後面跟着一個數組,因爲數組原生支持遍歷器,所以就會遍歷數組成員。yield命令後面若是不加星號,返回的是整個數組,加了星號就表示返回的是數組的遍歷器對象。
function* gen(){
  yield* ["a", "b", "c"];
}
gen().next() // { value:"a", done:false }
複製代碼
  • 任何數據結構只要有 Iterator 接口,就能夠被yield*遍歷。
let read = (function* () {
  yield 'hello';
  yield* 'hello';
})();

read.next().value // "hello"
read.next().value // "h"
複製代碼
  • 若是被代理的 Generator 函數有return語句,那麼就能夠向代理它的 Generator 函數返回數據。下面例子中函數fooreturn語句,向函數bar提供了返回值。
function* foo() {
  yield 2;
  yield 3;
  return "foo";
}

function* bar() {
  yield 1;
  var v = yield* foo();
  console.log("v: " + v);
  yield 4;
}

var 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}
複製代碼

做爲對象屬性的 Generator 函數寫法

let obj = {
  * myGeneratorMethod() {
    ···
  }
};
// 等價
let obj = {
  myGeneratorMethod: function* () { // *位置能夠在function關鍵字和括號之間任意位置
    // ···
  }
};
複製代碼

Generator 函數的this

function* g() {}
g.prototype.hello = function () {
  return 'hi!';
};
let obj = g();
obj instanceof g // true
obj.hello() // 'hi!'
複製代碼
  • Generator 函數g返回的遍歷器obj,是g的實例,並且繼承了g.prototype。可是,把g看成普通的構造函數,並不會生效,由於g返回的老是遍歷器對象,而不是this對象。
  • **Generator函數也不能跟new命令一塊兒用,會報錯。
    function* F() {
      yield this.x = 2;
      yield this.y = 3;
    }
    
    new F()
    // TypeError: F is not a constructor
    複製代碼
    • 變通方法。首先,生成一個空對象,使用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
    複製代碼
    • 上面執行的是遍歷器對象f,可是生成的對象實例是obj,將obj換成F.prototype。再將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
    複製代碼

應用

  1. 取出嵌套數組的全部成員
    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
    複製代碼
  2. 遍歷徹底二叉樹。
    // 下面是二叉樹的構造函數,
    // 三個參數分別是左樹、當前節點和右樹
    function Tree(left, label, right) {
      this.left = left;
      this.label = label;
      this.right = right;
    }
    
    // 下面是中序(inorder)遍歷函數。
    // 因爲返回的是一個遍歷器,因此要用generator函數。
    // 函數體內採用遞歸算法,因此左樹和右樹要用yield*遍歷
    function* inorder(t) {
      if (t) {
        yield* inorder(t.left);
        yield t.label;
        yield* inorder(t.right);
      }
    }
    
    // 下面生成二叉樹
    function make(array) {
      // 判斷是否爲葉節點
      if (array.length == 1) return new Tree(null, array[0], null);
      return new Tree(make(array[0]), array[1], make(array[2]));
    }
    let tree = make([[['a'], 'b', ['c']], 'd', [['e'], 'f', ['g']]]);
    
    // 遍歷二叉樹
    var result = [];
    for (let node of inorder(tree)) {
      result.push(node);
    }
    
    result
    // ['a', 'b', 'c', 'd', 'e', 'f', 'g']
    複製代碼
  3. Ajax 的異步操做
    • 經過 Generator 函數部署 Ajax 操做,能夠用同步的方式表達。注意,makeAjaxCall函數中的next方法,必須加上response參數,由於yield表達式,自己是沒有值的,老是等於undefined
    function* main() {
      var result = yield request("http://some.url");
      var resp = JSON.parse(result);
        console.log(resp.value);
    }
    
    function request(url) {
      makeAjaxCall(url, function(response){
        it.next(response);
      });
    }
    
    var it = main();
    it.next();
    複製代碼
  4. 逐行讀取文本文件。
    function* numbers() {
      let file = new FileReader("numbers.txt");
      try {
        while(!file.eof) {
          yield parseInt(file.readLine(), 10);
        }
      } finally {
        file.close();
      }
    }
    複製代碼
  5. 控制流管理
    • 若是有一個多步操做很是耗時,採用回調函數,可能會寫成下面這樣。
    step1(function (value1) {
      step2(value1, function(value2) {
        step3(value2, function(value3) {
          step4(value3, function(value4) {
            // Do something with value4
          });
        });
      });
    });
    複製代碼
    • 利用for...of循環會自動依次執行yield命令的特性,提供一種更通常的控制流管理的方法。
    let steps = [step1Func, step2Func, step3Func];
    
    function* iterateSteps(steps){
      for (var i=0; i< steps.length; i++){
        var step = steps[i];
        yield step();
      }
    }
    複製代碼
  6. 部署 Iterator 接口
    • 利用 Generator 函數,能夠在任意對象上部署 Iterator 接口。
    function* iterEntries(obj) {
      let keys = Object.keys(obj);
      for (let i=0; i < keys.length; i++) {
        let key = keys[i];
        yield [key, obj[key]];
      }
    }
    
    let myObj = { foo: 3, bar: 7 };
    
    for (let [key, value] of iterEntries(myObj)) {
      console.log(key, value);
    }
    
    // foo 3
    // bar 7
    複製代碼
  7. 做爲數據結構
    • Generator 能夠看做是數據結構,更確切地說,能夠看做是一個數組結構。
    function* doStuff() {
      yield fs.readFile.bind(null, 'hello.txt');
      yield fs.readFile.bind(null, 'world.txt');
      yield fs.readFile.bind(null, 'and-such.txt');
    }
    
    for (task of doStuff()) {
      // task是一個函數,能夠像回調函數那樣使用它
    }
    複製代碼
  8. 異步應用
相關文章
相關標籤/搜索