ES6躬行記(19)——生成器

  根據ES6制訂的標準自定義迭代器實現起來比較複雜,所以ES6又引入了生成器的概念,生成器(Generator)是一個能直接建立並返回迭代器的特殊函數,可將其賦給可迭代對象的Symbol.iterator屬性。與普通函數不一樣,生成器不只能夠暫停函數內部的執行(即維護內部的狀態),在聲明時還須要包含一個星號(*),而且擁有next()、return()和throw()三個迭代器方法。php

1、function*

  生成器在聲明時,須要把星號加到function關鍵字與函數名之間,但ES6沒有規定星號兩邊是否須要空格,所以下面四種寫法都是容許的,本篇將採用第一種寫法。html

function* generator() {}
function*generator() {}
function *generator() {}
function * generator() {}

  生成器也能經過函數表達式建立,以下代碼所示。注意,不能用箭頭函數建立生成器。編程

var iterator = function* () {};

  生成器雖然不能做爲構造函數使用,但能夠是對象的一個方法,而且還支持第5篇提到的簡潔方式的寫法,以下所示。json

var obj = {
  *generator() {}
};

2、yield

  生成器之因此能在其內部實現分批執行,還要多虧ES6新增的yield關鍵字。這個關鍵字可標記暫停位置,具體使用可參考下面的代碼。數組

function* generator() {
  var count = 0;
  while (count < 2)
    yield count++;
  return count;
}
var iterator = generator();

  雖然生成器的調用方式和普通函數相同,但它不會立刻返回函數的結果(即不能馬上執行)。而是先返回一個它所生成的迭代器,而後再調用其next()方法恢復內部語句的執行(以下代碼所示),直至遇到yield關鍵字,再次暫停,如此反覆,一直持續到函數的末尾或碰到return語句才終止這套循環操做。異步

iterator.next();        //{value: 0, done: false}
iterator.next();        //{value: 1, done: false}
iterator.next();        //{value: 2, done: true}

1)yield表達式異步編程

  yield關鍵字的後面能夠跟一個表達式,例如代碼中的count++。生成器的next()方法可以返回一個IteratorResult對象,其done屬性用於判斷生成器是否執行完畢,便是否還有yield表達式。關於IteratorResult兩個屬性的值,須要分狀況說明,具體以下所列。函數

(1)當生成器還在執行時,value的值可經過計算yield表達式獲得,done的值爲false。fetch

(2)當生成器執行完畢時,value的值是undefined,done的值爲true。this

(3)當遇到return語句時,value的值就是return後面跟的值,done的值爲true。

  要想遍歷生成器,除了藉助next()方法以外,還可使用for-of循環。但要注意,遍歷到的是yield表達式的計算結果,以下所示。

/********************
  0
  1
********************/
for(var step of iterator) {
  console.log(step);
}

2)優先級和結合性

  由於yield能夠單獨使用(例如x=yield),因此它並非一個運算符。雖然如此,但它仍是包含優先級和結合性的概念。yield的優先級很低,僅比擴展運算符和逗號高,若是要提早計算,能夠像下面這樣用一對圓括號包裹。

1 + (yield 2);

  yield的結合性與等號同樣,也是從右往左,例如yield yield 1至關於yield(yield 1)。另外,yield有一個很重要的限制,就是它只能存在於生成器內,在其它位置出現都會有異常,包括生成器中的子函數內,以下所示。

function* error() {
  function inner() {
    yield 1;
  }
}

3、3個方法

1)next()

  本節開篇的時候曾提到過生成器包含三個迭代器方法,接下來將圍繞這三個方法展開講解。首先介紹的是next()方法,它能接收一個參數,而這個參數會成爲上一個yield表達式的返回值。如下面的代碼爲例,calculate()函數包含兩個yield表達式,在建立生成器後,調用了兩次next()方法,第一次沒有傳參,第二次傳入的數字10被賦給了x變量。

function* calculate() {
  let x = yield 1;
  let y = yield x + 2;
  return y;
}
var iterator = calculate();
iterator.next();          //{value: 1, done: false}
iterator.next(10);        //{value: 12, done: false}

  注意,第一次調用next()方法時,即便傳進了參數,這個參數也會被忽略,由於此時還不存在上一個yield表達式。

2)return()

  接下來介紹的是return()方法,它能提早終止當前生成器,相似於在函數體內立刻執行return語句。下面沿用上一個示例,將函數名改爲stop,第二次調用的方法改爲return()。

function* stop() {
  let x = yield 1;
  let y = yield x + 2;
  return y;
}
var iterator = stop();
iterator.next();        //{value: 1, done: false}
iterator.return(10);    //{value: 10, done: true}
iterator.next();        //{value: undefined, done: true}

  return()方法也能接收一個參數,而從上面的調用結果中能夠得知,這個參數至關於return運算符後面跟的值,以下所示。

function* stop() {
  let x = yield 1;
  return 10;
}

3)throw()

  最後介紹的是throw()方法,它能強制生成器拋出一個錯誤。此方法也有一個參數,但這個參數只能被try-catch語句中的catch部分接收。下面用一個例子演示throw()方法的具體使用。

function* especial() {
  var count = 1;
  try {
    yield count;
  } catch (e) {
    count = 2;
    console.log(e);            //"inner"
  }
  yield count + 3;
}
var iterator = especial();
iterator.next();              //{value: 1, done: false}
try {
  iterator.throw("inner");     //{value: 5, done: false}
  iterator.next();             //{value: undefined, done: true}
  iterator.throw("outer");
} catch (e) {
  console.log(e);              //"outer"
}

  在especial生成器的內部和外部各有一條try-catch語句。第一次調用throw()方法,在生成器內部先捕獲拋出的錯誤,再把傳入的字符串「inner」賦給catch的e參數,接着執行yield count + 3,最後返回一個計算過的IteratorResult對象。第二次調用throw()方法,因爲生成器已執行完畢,所以只能在外部將錯誤捕獲。

4、yield*

  在yield關鍵字後面跟一個星號(兩邊的空格可選),就能將執行權委託給另外一個生成器或可迭代對象。如下面代碼爲例,在delegation生成器中,有兩個yield*表達式,第一個跟的是數組,第二個跟的是generator生成器(至關於將兩個生成器合併)。

function* generator() {
  var count = 0;
  while (count < 2)
    yield count++;
  return count;
}
function* delegation() {
  yield* ["a", "b"];
  var result = yield* generator();
  console.log(result);      //2
}
var iterator = delegation();
iterator.next();            //{value: "a", done: false}
iterator.next();            //{value: "b", done: false}
iterator.next();            //{value: 0, done: false}
iterator.next();            //{value: 1, done: false}
iterator.next();            //{value: undefined, done: true}

  從上面的遍歷結果中可知,delegation生成器先訪問數組的每一個元素,再計算generator生成器中的yield表達式,並將其返回值賦給了result變量。

5、異步編程

  在ES6以前,要實現異步編程,最經常使用的方法是用回調函數,例如捕獲Ajax通訊中的響應內容,以下所示。

function fetch(callback) {
  $.getJSON("server.php", {}, function(json) {
    callback.call(this, json);
  });
}
function asyn() {
  fetch1(function(json) {
    console.log(json);        //{code: 200, msg: "操做成功"}
  });
}
asyn();

  fetch()函數調用了jQuery中能發起Ajax請求的getJSON()方法,在其載入成功時的回調函數內間接調用了callback參數(即傳遞進來的回調函數),其參數就是響應內容。

  接下來將asyn()變爲生成器,並在其內部添加yield表達式,而後在getJSON()的回調函數中調用生成器的next()方法,並將響應內容做爲參數傳入。

function fetch() {
  $.getJSON("server.php", {}, function(json) {
    gen.next(json);
  });
}
function* asyn() {
  var result = yield fetch();
  console.log(result);        //{code: 200, msg: "操做成功"}
}
var gen = asyn();
gen.next();

  經過上面的代碼可知,生成器能用同步的方式實現異步編程,從而有效避免了層層嵌套的回調金字塔。

相關文章
相關標籤/搜索