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

一、function*

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

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

  生成器也能通過函數表達式創建,如下代碼所示。注意,不能用箭頭函數創建生成器。

var iterator = function* () {};

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

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

二、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。

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

(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個方法

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()方法,由於生成器已執行完畢,因此只能在外部將錯誤捕獲。

四、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變數。

五、非同步編程

  在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();

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


推薦閱讀:
相关文章