ES6中的迭代器(Iterator)和生成器(Generator)

 前面的話

  用循環語句迭代數據時,必需要初始化一個變量來記錄每一次迭代在數據集合中的位置,而在許多編程語言中,已經開始經過程序化的方式用迭代器對象返回迭代過程當中集合的每個元素編程

  迭代器的使用能夠極大地簡化數據操做,因而ES6也向JS中添加了這個迭代器特性。新的數組方法和新的集合類型(如Set集合與Map集合)都依賴迭代器的實現,這個新特性對於高效的數據處理而言是不可或缺的,在語言的其餘特性中也都有迭代器的身影:新的for-of循環、展開運算符(...),甚至連異步編程均可以使用迭代器json

  本文將詳細介紹ES6中的迭代器(Iterator)和生成器(Generator)數組

 

引入

  下面是一段標準的for循環代碼,經過變量i來跟蹤colors數組的索引,循環每次執行時,若是i小於數組長度len則加1,並執行下一次循環瀏覽器

var colors = ["red", "green", "blue"];
for (var i = 0, len = colors.length; i < len; i++) {
    console.log(colors[i]);
}

  雖然循環語句語法簡單,但若是將多個循環嵌套則須要追蹤多個變量,代碼複雜度會大大增長,一不當心就錯誤使用了其餘for循環的跟蹤變量,從而致使程序出錯。迭代器的出現旨在消除這種複雜性並減小循環中的錯誤app

 

迭代器

  迭代器是一種特殊對象,它具備一些專門爲迭代過程設計的專有接口,全部的迭代器對象都有一個next()方法,每次調用都返回一個結果對象。結果對象有兩個屬性:一個是value,表示下一個將要返回的值;另外一個是done,它是一個布爾類型的值,當沒有更多可返回數據時返回true。迭代器還會保存一個內部指針,用來指向當前集合中值的位置,每調用一次next()方法,都會返回下一個可用的值異步

  若是在最後一個值返回後再調用next()方法,那麼返回的對象中屬性done的值爲true,屬性value則包含迭代器最終返回的值,這個返回值不是數據集的一部分,它與函數的返回值相似,是函數調用過程當中最後一次給調用者傳遞信息的方法,若是沒有相關數據則返回undefined編程語言

  下面用ES5的語法建立一個迭代器異步編程

function createIterator(items) {
    var i = 0;
    return {
        next: function() {
            var done = (i >= items.length);
            var value = !done ? items[i++] : undefined;
            return {
                done: done,
                value: value
            };
        }
    };
}
var iterator = createIterator([1, 2, 3]);
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next()); // "{ value: 2, done: false }"
console.log(iterator.next()); // "{ value: 3, done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"
// 以後的全部調用
console.log(iterator.next()); // "{ value: undefined, done: true }"

  在上面這段代碼中,createIterator()方法返回的對象有一個next()方法,每次調用時,items數組的下一個值會做爲value返回。當i爲3時,done變爲true;此時三元表達式會將value的值設置爲undefined。最後兩次調用的結果與ES6迭代器的最終返回機制相似,當數據集被用盡後會返回最終的內容函數

  上面這個示例很複雜,而在ES6中,迭代器的編寫規則也一樣複雜,但ES6同時還引入了一個生成器對象,它可讓建立迭代器對象的過程變得更簡單工具

 

生成器

  生成器是一種返回迭代器的函數,經過function關鍵字後的星號(*)來表示,函數中會用到新的關鍵字yield。星號能夠緊挨着function關鍵字,也能夠在中間添加一個空格

// 生成器
function *createIterator() {
    yield 1;
    yield 2;
    yield 3;
}
// 生成器能像正規函數那樣被調用,但會返回一個迭代器
let iterator = createIterator();
console.log(iterator.next().value); // 1
console.log(iterator.next().value); // 2
console.log(iterator.next().value); // 3

  在這個示例中,createlterator()前的星號代表它是一個生成器;yield關鍵字也是ES6的新特性,能夠經過它來指定調用迭代器的next()方法時的返回值及返回順序。生成迭代器後,連續3次調用它的next()方法返回3個不一樣的值,分別是一、2和3。生成器的調用過程與其餘函數同樣,最終返回的是建立好的迭代器

  生成器函數最有趣的部分是,每當執行完一條yield語句後函數就會自動中止執行。舉個例子,在上面這段代碼中,執行完語句yield 1以後,函數便再也不執行其餘任何語句,直到再次調用迭代器的next()方法纔會繼續執行yield 2語句。生成器函數的這種停止函數執行的能力有不少有趣的應用

  使用yield關鍵字能夠返回任何值或表達式,因此能夠經過生成器函數批量地給迭代器添加元素。例如,能夠在循環中使用yield關鍵字

function *createIterator(items) {
    for (let i = 0; i < items.length; i++) {
        yield items[i];
    }
}
let iterator = createIterator([1, 2, 3]);
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next()); // "{ value: 2, done: false }"
console.log(iterator.next()); // "{ value: 3, done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"
// 以後的全部調用
console.log(iterator.next()); // "{ value: undefined, done: true }"

  在此示例中,給生成器函數createlterator()傳入一個items數組,而在函數內部,for循環不斷從數組中生成新的元素放入迭代器中,每遇到一個yield語句循環都會中止;每次調用迭代器的next()方法,循環會繼續運行並執行下一條yield語句

  生成器函數是ES6中的一個重要特性,能夠將其用於全部支持函數使用的地方

【使用限制】

  yield關鍵字只可在生成器內部使用,在其餘地方使用會致使程序拋出錯誤

function *createIterator(items) {
    items.forEach(function(item) {
        // 語法錯誤
        yield item + 1;
    });
}

  從字面上看,yield關鍵字確實在createlterator()函數內部,可是它與return關鍵字同樣,兩者都不能穿透函數邊界。嵌套函數中的return語句不能用做外部函數的返回語句,而此處嵌套函數中的yield語句會致使程序拋出語法錯誤

【生成器函數表達式】

  也能夠經過函數表達式來建立生成器,只需在function關鍵字和小括號中間添加一個星號(*)便可

let createIterator = function *(items) {
    for (let i = 0; i < items.length; i++) {
        yield items[i];
    }
};
let iterator = createIterator([1, 2, 3]);
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next()); // "{ value: 2, done: false }"
console.log(iterator.next()); // "{ value: 3, done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"
// 以後的全部調用
console.log(iterator.next()); // "{ value: undefined, done: true }"

  在這段代碼中,createlterator()是一個生成器函數表達式,而不是一個函數聲明。因爲函數表達式是匿名的,所以星號直接放在function關鍵字和小括號之間。此外,這個示例基本與前例相同,使用的也是for循環

  [注意]不能用箭頭函數來建立生成器

【生成器對象的方法】

  因爲生成器自己就是函數,於是能夠將它們添加到對象中。例如,在ES5風格的對象字面量中,能夠經過函數表達式來建立生成器

var o = {
    createIterator: function *(items) {
            for (let i = 0; i < items.length; i++) {
                yield items[i];
            }
        }
};
let iterator = o.createIterator([1, 2, 3]);

  也能夠用ES6的函數方法的簡寫方式來建立生成器,只需在函數名前添加一個星號(*)

var o = {
    *createIterator(items) {
            for (let i = 0; i < items.length; i++) {
                yield items[i];
            }
        }
};
let iterator = o.createIterator([1, 2, 3]);

  這些示例使用了不一樣於以前的語法,但它們的功能其實是等價的。在簡寫版本中,因爲不使用function關鍵字來定義createlterator()方法,所以儘管能夠在星號和方法名之間留白,但仍是將星號緊貼在方法名以前

【狀態機】

  生成器的一個經常使用功能是生成狀態機

let state = function*(){
    while(1){
        yield 'A';
        yield 'B';
        yield 'C';
    }
}

let status = state();
console.log(status.next().value);//'A'
console.log(status.next().value);//'B'
console.log(status.next().value);//'C'
console.log(status.next().value);//'A'
console.log(status.next().value);//'B'

 

可迭代對象

  可迭代對象具備Symbol.iterator屬性,是一種與迭代器密切相關的對象。Symbol.iterator經過指定的函數能夠返回一個做用於附屬對象的迭代器。在ES6中,全部的集合對象(數組、Set集合及Map集合)和字符串都是可迭代對象,這些對象中都有默認的迭代器。ES6中新加入的特性for-of循環須要用到可迭代對象的這些功能

  [注意]因爲生成器默認會爲Symbol.iterator屬性賦值,所以全部經過生成器建立的迭代器都是可迭代對象

  一開始,咱們曾提到過循環內部索引跟蹤的相關問題,要解決這個問題,須要兩個工具:一個是迭代器,另外一個是for-of循環。如此一來,便不須要再跟蹤整個集合的索引,只需關注集合中要處理的內容

  for-of循環每執行一次都會調用可迭代對象的next()方法,並將迭代器返回的結果對象的value屬性存儲在一個變量中,循環將持續執行這一過程直到返回對象的done屬性的值爲true。這裏有個示例

let values = [1, 2, 3];
for (let num of values) {
    //1
    //2
    //3
    console.log(num);
}

  這段for-of循環的代碼經過調用values數組的Symbol.iterator方法來獲取迭代器,這一過程是在JS引擎背後完成的。隨後迭代器的next()方法被屢次調用,從其返回對象的value屬性讀取值並存儲在變量num中,依次爲一、2和3,當結果對象的done屬性值爲true時循環退出,因此num不會被賦值爲undefined

  若是隻需迭代數組或集合中的值,用for-of循環代替for循環是個不錯的選擇。相比傳統的for循環,for-of循環的控制條件更簡單,不須要追蹤複雜的條件,因此更少出錯

  [注意]若是將for-of語句用於不可迭代對象、null或undefined將會致使程序拋出錯誤

【訪問默認迭代器】

  能夠經過Symbol.iterator來訪問對象默認的迭代器

let values = [1, 2, 3];
let iterator = values[Symbol.iterator]();
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next()); // "{ value: 2, done: false }"
console.log(iterator.next()); // "{ value: 3, done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"

  在這段代碼中,經過Symbol.iterator獲取了數組values的默認迭代器,並用它遍歷數組中的元素。在JS引擎中執行for-of循環語句時也會有相似的處理過程

  因爲具備Symbol.iterator屬性的對象都有默認的迭代器,所以能夠用它來檢測對象是否爲可迭代對象

function isIterable(object) {
    return typeof object[Symbol.iterator] === "function";
}
console.log(isIterable([1, 2, 3])); // true
console.log(isIterable("Hello")); // true
console.log(isIterable(new Map())); // true
console.log(isIterable(new Set())); // true
console.log(isIterable(new WeakMap())); // false
console.log(isIterable(new WeakSet())); // false

  這裏的islterable()函數能夠檢查指定對象中是否存在默認的函數類型迭代器,而for-of循環在執行前也會作類似的檢查

  除了使用內建的可迭代對象類型的Symbol.iterator,也可使用Symbol.iterator來建立屬於本身的迭代器

【建立可迭代對象】

  默認狀況下,開發者定義的對象都是不可迭代對象,但若是給Symbol.iterator屬性添加一個生成器,則能夠將其變爲可迭代對象

let collection = {
    items: [],
    *[Symbol.iterator]() {
        for (let item of this.items) {
            yield item;
        }
    }
};
collection.items.push(1);
collection.items.push(2);
collection.items.push(3);
for (let x of collection) {
    //1
    //2
    //3
    console.log(x);
}

  在這個示例中,先建立一個生成器(注意,星號仍然在屬性名前)並將其賦值給對象的Symbol.iterator屬性來建立默認的迭代器;而在生成器中,經過for-of循環迭代this.items並用yield返回每個值。collection對象默認迭代器的返回值由迭代器this.items自動生成,而非手動遍從來定義返回值

【展開運算符和非數組可迭代對象】

  經過展開運算符(...)能夠把Set集合轉換成一個數組

let set = new Set([1, 2, 3, 3, 3, 4, 5]),
array = [...set]; console.log(array); // [1,2,3,4,5]

  這段代碼中的展開運算符把Set集合的全部值填充到了一個數組字面量裏,它能夠操做全部可迭代對象,並根據默認迭代器來選取要引用的值,從迭代器讀取全部值。而後按照返回順序將它們依次插入到數組中。Set集合是一個可迭代對象,展開運算符也能夠用於其餘可迭代對象

let map = new Map([ ["name", "huochai"], ["age", 25]]),
array = [...map]; console.log(array); // [ ["name", "huochai"], ["age", 25]]

  展開運算符把Map集合轉換成包含多個數組的數組,Map集合的默認迭代器返回的是多組鍵值對,因此結果數組與執行new Map()時傳入的數組看起來同樣

  在數組字面量中能夠屢次使用展開運算符,將可迭代對象中的多個元素依次插入新數組中,替換原先展開運算符所在的位置

let smallNumbers = [1, 2, 3],
bigNumbers = [100, 101, 102], allNumbers = [0, ...smallNumbers, ...bigNumbers]; console.log(allNumbers.length); // 7 console.log(allNumbers); // [0, 1, 2, 3, 100, 101, 102]

  建立一個變量allNumbers,用展開運算符將smallNumbers和bigNumbers裏的值依次添加到allNumbers中。首先存入0,而後存入small中的值,最後存入bigNumbers中的值。固然,原始數組中的值只是被複制到allNumbers中,它們自己並未改變

  因爲展開運算符能夠做用於任意可迭代對象,所以若是想將可迭代對象轉換爲數組,這是最簡單的方法。既能夠將字符串中的每個字符(不是編碼單元)存入新數組中,也能夠將瀏覽器中NodeList對象中的每個節點存入新的數組中

 

內建迭代器

  迭代器是ES6的一個重要組成部分,在ES6中,已經默認爲許多內建類型提供了內建迭代器,只有當這些內建迭代器沒法實現目標時才須要本身建立。一般來講當定義本身的對象和類時纔會遇到這種狀況,不然,徹底能夠依靠內建的迭代器完成工做,而最常使用的多是集合的那些迭代器

【集合對象迭代器】

  在ES6中有3種類型的集合對象:數組、Map集合與Set集合

  爲了更好地訪問對象中的內容,這3種對象都內建瞭如下三種迭代器

entries() 返回一個迭代器,其值爲多個鍵值對
values() 返回一個迭代器,其值爲集合的值
keys() 返回一個迭代器,其值爲集合中的全部鍵名

  調用以上3個方法均可以訪問集合的迭代器

entries()迭代器

  每次調用next()方法時,entries()迭代器都會返回一個數組,數組中的兩個元素分別表示集合中每一個元素的鍵與值。若是被遍歷的對象是數組,則第一個元素是數字類型的索引;若是是Set集合,則第一個元素與第二個元素都是值(Set集合中的值被同時做爲鍵與值使用);若是是Map集合,則第一個元素爲鍵名

let colors = [ "red", "green", "blue" ];
let tracking = new Set([1234, 5678, 9012]);
let data = new Map();
data.set("title", "Understanding ES6");
data.set("format", "ebook");
for (let entry of colors.entries()) {
    console.log(entry);
}
for (let entry of tracking.entries()) {
    console.log(entry);
}
for (let entry of data.entries()) {
    console.log(entry);
}

  調用console.log()方法後輸出如下內容

[0, "red"]
[1, "green"]
[2, "blue"]
[1234, 1234]
[5678, 5678]
[9012, 9012]
["title", "Understanding ES6"]
["format", "ebook"]

  在這段代碼中,調用每一個集合的entries()方法獲取一個迭代器,並使用for-of循環來遍歷元素,且經過console將每個對象的鍵值對輸出出來

values()迭代器

  調用values()迭代器時會返回集合中所存的全部值

let colors = [ "red", "green", "blue" ];
let tracking = new Set([1234, 5678, 9012]);
let data = new Map();
data.set("title", "Understanding ES6");
data.set("format", "ebook");
for (let value of colors.values()) {
    console.log(value);
}
for (let value of tracking.values()) {
    console.log(value);
}
for (let value of data.values()) {
    console.log(value);
}

  調用console.log()方法後輸出如下內容

"red"
"green"
"blue"
1234
5678
9012
"Understanding ES6"
"ebook"

  如上所示,調用values()迭代器後,返回的是每一個集合中包含的真正數據,而不包含數據在集合中的位置信息

keys()迭代器

  keys()迭代器會返回集合中存在的每個鍵。若是遍歷的是數組,則會返回數字類型的鍵,數組自己的其餘屬性不會被返回;若是是Set集合,因爲鍵與值是相同的,所以keys()和values()返回的也是相同的迭代器;若是是Map集合,則keys()迭代器會返回每一個獨立的鍵

let colors = [ "red", "green", "blue" ];
let tracking = new Set([1234, 5678, 9012]);
let data = new Map();
data.set("title", "Understanding ES6");
data.set("format", "ebook");
for (let key of colors.keys()) {
    console.log(key);
}
for (let key of tracking.keys()) {
    console.log(key);
}
for (let key of data.keys()) {
    console.log(key);
}

  調用console.log()方法後輸出如下內容

0
1
2
1234
5678
9012
"title"
"format"

  keys()迭代器會獲取colors、tracking和data這3個集合中的每個鍵,並且分別在3個for-of循環內部將這些鍵名打印出來。對於數組對象來講,不管是否爲數組添加命名屬性,打印出來的都是數字類型的索引;而for-in循環迭代的是數組屬性而不是數字類型的索引

不一樣集合類型的默認迭代器

  每一個集合類型都有一個默認的迭代器,在for-of循環中,若是沒有顯式指定則使用默認的迭代器。數組和Set集合的默認迭代器是values()方法,Map集合的默認迭代器是entries()方法。有了這些默認的迭代器,能夠更輕鬆地在for-of循環中使用集合對象

let colors = [ "red", "green", "blue" ];
let tracking = new Set([1234, 5678, 9012]);
let data = new Map();
data.set("title", "Understanding ES6");
data.set("format", "print");
// 與使用 colors.values() 相同
for (let value of colors) {
    console.log(value);
}
// 與使用 tracking.values() 相同
for (let num of tracking) {
    console.log(num);
}
// 與使用 data.entries() 相同
for (let entry of data) {
    console.log(entry);
}

  上述代碼未指定迭代器,因此將使用默認的迭代器。數組、Set集合及Map集合的默認迭代器也會反應出這些對象的初始化過程,因此這段代碼會輸出如下內容

"red"
"green"
"blue"
1234
5678
9012
["title", "Understanding ES6"]
["format", "print"]

  默認狀況下,若是是數組和Set集合,會逐一返回集合中全部的值。若是是Map集合,則按照Map構造函數參數的格式返回相同的數組內容。而WeakSet集合與WeakMap集合就沒有內建的迭代器,因爲要管理弱引用,於是沒法確切地知道集合中存在的值,也就沒法迭代這些集合了

【字符串迭代器】

  自ES5發佈之後,JS字符串慢慢變得更像數組了,例如,ES5正式規定能夠經過方括號訪問字符串中的字符(也就是說,text[0]能夠獲取字符串text的第一個字符,並以此類推)。因爲方括號操做的是編碼單元而非字符,所以沒法正確訪問雙字節字符

var message = "A 𠮷 B" ;
for (let i=0; i < message.length; i++) {
    console.log(message[i]);
}

  在這段代碼中,訪問message的length屬性獲取索引值,並經過方括號訪問來迭代並打印一個單字符字符串,可是輸出的結果卻與預期不符

A
 
 
 
 
B

  因爲雙字節字符被視做兩個獨立的編碼單元,從而最終在A與B之間打印出4個空行

  所幸,ES6的目標是全面支持Unicode,而且咱們能夠經過改變字符串的默認迭代器來解決這個問題,使其操做字符而不是編碼單元。如今,修改前一個示例中字符串的默認迭代器,讓for-of循環輸出正確的內容

var message = "A 𠮷 B" ;
for (let c of message) {
    console.log(c);
}

  這段代碼輸出如下內容

A
 
𠮷
 
B

  這個結果更符合預期,經過循環語句能夠直接操做字符併成功打印出Unicode字符

【NodeList迭代器】

  DOM標準中有一個NodeList類型,document對象中的全部元素都用這個類型來表示。對於編寫Web瀏覽器環境中的JS開發者來講,須要花點兒功夫去理解NodeList對象和數組之間的差別。兩者都使用length屬性來表示集合中元素的數量,均可以經過方括號來訪問集合中的獨立元素。而在內部實現中,兩者的表現很是不一致,於是會形成不少困擾

  自從ES6添加了默認迭代器後,DOM定義中的NodeList類型(定義在HTML標準而不是ES6標準中)也擁有了默認迭代器,其行爲與數組的默認迭代器徹底一致。因此能夠將NodeList應用於for-of循環及其餘支持對象默認迭代器的地方

var divs = document.getElementsByTagName("div");
for (let div of divs) {
    console.log(div.id);
}

  在這段代碼中,經過調用getElementsByTagName()方法獲取到document對象中全部div元素的列表,在for-of循環中遍歷列表中的每個元素並輸出元素ID,其實是按照處理數組的方式來處理NodeList的

 

高級迭代器

  迭代器的基礎功能能夠輔助完成不少任務,經過生成器建立迭代器的過程也很便捷,除了這些簡單的集合遍歷任務以外,迭代器也能夠被用於完成一些複雜的任務

【給迭代器傳遞參數】

  迭代器既能夠用迭代器的next()方法返回值,也能夠在生成器內部使用yield關鍵字來生成值。若是給迭代器的next()方法傳遞參數,則這個參數的值就會替代生成器內部上條yield語句的返回值。而若是要實現更多像異步編程這樣的高級功能,那麼這種給迭代器傳值的能力就變得相當重要

function *createIterator() {
    let first = yield 1;
    let second = yield first + 2; // 4 + 2
    yield second + 3; // 5 + 3
}
let iterator = createIterator();
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next(4)); // "{ value: 6, done: false }"
console.log(iterator.next(5)); // "{ value: 8, done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"

  第一次調用next()方法時不管傳入什麼參數都會被丟棄。因爲傳給next()方法的參數會替代上一次yield的返回值,而在第一次調用next()方法前不會執行任何yield語句,所以在第一次調用next()方法時傳遞參數是毫無心義的

  第二次調用next()方法傳入數值4做爲參數,它最後被賦值給生成器函數內部的變量first。在一個含參yield語句中,表達式右側等價於第一次調用next()方法後的下一個返回值,表達式左側等價於第二次調用next()方法後,在函數繼續執行前獲得的返回值。第二次調用next()方法傳入的值爲4,它會被賦值給變量first,函數則繼續執行。第二條yield語句在第一次yield的結果上加了2,最終的返回值爲6

  第三次調用next()方法時,傳入數值5,這個值被賦值給second,最後用於第三條yield語句並最終返回數值8

【在迭代器中拋出錯誤】  

  除了給迭代器傳遞數據外,還能夠給它傳遞錯誤條件。經過throw()方法,當迭代器恢復執行時可令其拋出一個錯誤。這種主動拋出錯誤的能力對於異步編程而言相當重要,也能提供模擬結束函數執行的兩種方法(返回值或拋出錯誤),從而加強生成器內部的編程彈性。將錯誤對象傳給throw()方法後,在迭代器繼續執行時其會被拋出

function *createIterator() {
    let first = yield 1;
    let second = yield first + 2; // yield 4 + 2 ,而後拋出錯誤
    yield second + 3; // 永不會被執行
}
let iterator = createIterator();
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next(4)); // "{ value: 6, done: false }"
console.log(iterator.throw(new Error("Boom"))); // 從生成器中拋出了錯誤

  在這個示例中,前兩個表達式正常求值,而調用throw()方法後,在繼續執行let second求值前,錯誤就會被拋出並阻止了代碼繼續執行。這個過程與直接拋出錯誤很類似,兩者惟一的區別是拋出的時機不一樣

  能夠在生成器內部經過try-catch代碼塊來捕獲這些錯誤

function *createIterator() {
    let first = yield 1;
    let second;
    try {
        second = yield first + 2; // yield 4 + 2 ,而後拋出錯誤
    } catch (ex) {
        second = 6; // 當出錯時,給變量另外賦值
    }
    yield second + 3;
}
let iterator = createIterator();
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next(4)); // "{ value: 6, done: false }"
console.log(iterator.throw(new Error("Boom"))); // "{ value: 9, done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"

  在此示例中,try-catch代碼塊包裹着第二條yield語句。儘管這條語句自己沒有錯誤,但在給變量second賦值前仍是會主動拋出錯誤,catch代碼塊捕獲錯誤後將second變量賦值爲6,下一條yield語句繼續執行後返回9

  這裏有一個有趣的現象調用throw()方法後也會像調用next()方法同樣返回一個結果對象。因爲在生成器內部捕獲了這個錯誤,於是會繼續執行下一條yield語句,最終返回數值9

  如此一來,next()和throw()就像是迭代器的兩條指令,調用next()方法命令迭代器繼續執行(可能提供一個值),調用throw()方法也會命令迭代器繼續執行,但同時也拋出一個錯誤,在此以後的執行過程取決於生成器內部的代碼

  在迭代器內部,若是使用了yield語句,則能夠經過next()方法和throw()方法控制執行過程,固然,也可使用return語句返回一些與普通函數返回語句不太同樣的內容

【生成器返回語句】

  因爲生成器也是函數,所以能夠經過return語句提早退出函數執行,對於最後一次next()方法調用,能夠主動爲其指定一個返回值。正如在其餘函數中那樣,能夠經過return語句指定一個返回值。而在生成器中,return表示全部操做已經完成,屬性done被設置爲true;若是同時提供了相應的值,則屬性value會被設置爲這個值

function *createIterator() {
    yield 1;
    return;
    yield 2;
    yield 3;
}
let iterator = createIterator();
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"

  這段代碼中的生成器包含多條yield語句和一條return語句,其中return語句緊隨第一條yield語句,其後的yield語句將不會被執行

  在return語句中也能夠指定一個返回值,該值將被賦值給返回對象的value屬性

function *createIterator() {
    yield 1;
    return 42;
}
let iterator = createIterator();
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next()); // "{ value: 42, done: true }"
console.log(iterator.next()); // "{ value: undefined, done: true }"

  在此示例中,第二次調用next()方法時返回對象的value屬性值爲42,done屬性首次設爲true;第三次調用next()方法依然返回一個對象,只是value屬性的值會變爲undefined。所以,經過return語句指定的返回值,只會在返回對象中出現一次,在後續調用返回的對象中,value屬性會被重置爲undefined

  [注意]展開運算符與for-of循環語句會直接忽略經過return語句指定的任何返回值,只要done一變爲true就當即中止讀取其餘的值。無論怎樣,迭代器的返回值依然是一個很是有用的特性

【委託生成器】

  在某些狀況下,咱們須要將兩個迭代器合二爲一,這時能夠建立一個生成器,再給yield語句添加一個星號,就能夠將生成數據的過程委託給其餘生成器。當定義這些生成器時,只需將星號放置在關鍵字yield和生成器的函數名之間便可

function *createNumberIterator() {
    yield 1;
    yield 2;
}
function *createColorIterator() {
    yield "red";
    yield "green";
}
function *createCombinedIterator() {
    yield *createNumberIterator();
    yield *createColorIterator();
    yield true;
}
var iterator = createCombinedIterator();
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next()); // "{ value: 2, done: false }"
console.log(iterator.next()); // "{ value: "red", done: false }"
console.log(iterator.next()); // "{ value: "green", done: false }"
console.log(iterator.next()); // "{ value: true, done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"

  這裏的生成器createCombinedIterator()前後委託了另外兩個生成器createNumberlterator()和createColorlterator()。僅根據迭代器的返回值來看,它就像是一個完整的迭代器,能夠生成全部的值。每一次調用next()方法就會委託相應的迭代器生成相應的值,直到最後由createNumberlterator()和cpeateColorlterator()建立的迭代器沒法返回更多的值,此時執行最後一條yield語句並返回true

  有了生成器委託這個新功能,能夠進一步利用生成器的返回值來處理複雜任務

function *createNumberIterator() {
    yield 1;
    yield 2;
    return 3;
}
function *createRepeatingIterator(count) {
    for (let i=0; i < count; i++) {
        yield "repeat";
    }
}
function *createCombinedIterator() {
    let result = yield *createNumberIterator();
    yield *createRepeatingIterator(result);
}
var iterator = createCombinedIterator();
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next()); // "{ value: 2, done: false }"
console.log(iterator.next()); // "{ value: "repeat", done: false }"
console.log(iterator.next()); // "{ value: "repeat", done: false }"
console.log(iterator.next()); // "{ value: "repeat", done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"

  在生成器createCombinedlterator()中,執行過程先被委託給了生成器createNumberlterator(),返回值會被賦值給變量result,執行到return 3時會返回數值3。這個值隨後被傳入createRepeatinglterator()做爲它的參數,於是生成字符串"repeat"的yield語句會被執行三次

  不管經過何種方式調用迭代器next()方法,數值3都不會被返回,它只存在於生成器createCombinedlterator()的內部。但若是想輸出這個值,則能夠額外添加一條yield語句

function *createNumberIterator() {
    yield 1;
    yield 2;
    return 3;
}
function *createRepeatingIterator(count) {
    for (let i=0; i < count; i++) {
        yield "repeat";
    }
}
function *createCombinedIterator() {
    let result = yield *createNumberIterator();
    yield result;
    yield *createRepeatingIterator(result);
}
var iterator = createCombinedIterator();
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next()); // "{ value: 2, done: false }"
console.log(iterator.next()); // "{ value: 3, done: false }"
console.log(iterator.next()); // "{ value: "repeat", done: false }"
console.log(iterator.next()); // "{ value: "repeat", done: false }"
console.log(iterator.next()); // "{ value: "repeat", done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"

  此處新添加的yield語句顯式地輸出了生成器createNumberlterator()的返回值。

  [注意]yield*也可直接應用於字符串,例如yield* "hello",此時將使用字符串的默認迭代器

 

異步任務執行

  生成器使人興奮的特性多與異步編程有關,JS中的異步編程有利有弊:簡單任務的異步化很是容易;而複雜任務的異步化會帶來不少管理代碼的挑戰。因爲生成器支持在函數中暫停代碼執行,於是能夠深刻挖掘異步處理的更多用法

  執行異步操做的傳統方式通常是調用一個函數並執行相應回調函數

let fs = require("fs");
fs.readFile("config.json", function(err, contents) {
    if (err) {
        throw err;
    }
    doSomethingWith(contents);
    console.log("Done");
});

  調用fs.readFile()方法時要求傳入要讀取的文件名和一個回調函數,操做結束後會調用該回調函數並檢查是否存在錯誤,若是沒有就能夠處理返回的內容。若是要執行的任務不多,那麼這樣的方式能夠很好地完成任務;如若須要嵌套回調或序列化一系列的異步操做,事情會變得很是複雜。此時,生成器和yield語句就派上用場了

【簡單任務執行器】

  因爲執行yield語句會暫停當前函數的執行過程並等待下一次調用next()方法,所以能夠建立一個函數,在函數中調用生成器生成相應的迭代器,從而在不用回調函數的基礎上實現異步調用next()方法

function run(taskDef) {
    // 建立迭代器,讓它在別處可用
    let task = taskDef();
    // 啓動任務
    let result = task.next();
    // 遞歸使用函數來保持對 next() 的調用
    function step() {
        // 若是還有更多要作的
        if (!result.done) {
            result = task.next();
            step();
        }
    }
    // 開始處理過程
    step();
}

  函數run()接受一個生成器函數做爲參數,這個函數定義了後續要執行的任務,生成一個迭代器並將它儲存在變量task中。首次調用迭代器的next()方法時,返回的結果被儲存起來稍後繼續使用。step()函數會檢查result.done的值,若是爲false則執行迭代器的next()方法,並再次執行step()操做。每次調用next()方法時,返回的最新信息總會覆寫變量result。在代碼的最後,初始化執行step()函數並開始整個的迭代過程,每次經過檢查result.done來肯定是否有更多任務須要執行

  藉助這個run()函數,能夠像這樣執行一個包含多條yield語句的生成器

run(function*() {
    console.log(1);
    yield;
    console.log(2);
    yield;
    console.log(3);
});

  這個示例最終會向控制檯輸出屢次調用next()方法的結果,分別爲數值一、2和3。固然,簡單輸出迭代次數不足以展現迭代器高級功能的實用之處,下一步將在迭代器與調用者之間互相傳值

【向任務執行器傳遞數據】

  給任務執行器傳遞數據的最簡單辦法是,將值經過迭代器的next()方法傳入做爲yield的生成值供下次調用。在這段代碼中,只需將result.value傳入next()方法便可

function run(taskDef) {
    // 建立迭代器,讓它在別處可用
    let task = taskDef();
    // 啓動任務
    let result = task.next();
    // 遞歸使用函數來保持對 next() 的調用
    function step() {
        // 若是還有更多要作的
        if (!result.done) {
            result = task.next(result.value);
            step();
        }
    }
    // 開始處理過程
    step();
}

  如今result.value做爲next()方法的參數被傳入,這樣就能夠在yield調用之間傳遞數據了

run(function*() {
    let value = yield 1;
    console.log(value); // 1
    value = yield value + 3;
    console.log(value); // 4
});

  此示例會向控制檯輸出兩個數值1和4。其中,數值1取自yield 1語句中回傳給變量value的值;而4取自給變量value加3後回傳給value的值。如今數據已經可以在yield調用間互相傳遞了,只需一個小小改變便能支持異步調用

【異步任務執行器】

  以前的示例只是在多個yield調用間來回傳遞靜態數據,而等待一個異步過程有些不一樣。任務執行器須要知曉回調函數是什麼以及如何使用它。因爲yield表達式會將值返回給任務執行器,全部的函數調用都會返回一個值,於是在某種程度上這也是一個異步操做,任務執行器會一直等待直到操做完成

  下面定義一個異步操做

function fetchData() {
    return function(callback) {
        callback(null, "Hi!");
    };
}

  本示例的原意是讓任務執行器調用的全部函數都返回一個能夠執行回調過程的函數,此處fetchData()函數的返回值是一個可接受回調函數做爲參數的函數,當調用它時會傳入一個字符串"Hi!"做爲回調函數的參數並執行。參數callback須要經過任務執行器指定,以確保回調函數執行時能夠與底層迭代器正確交互。儘管fetchData()是同步函數,但簡單添加一個延遲方法便可將其變爲異步函數

function fetchData() {
    return function(callback) {
        setTimeout(function() {
            callback(null, "Hi!");
        }, 50);
    };
}

  在這個版本的fetchData()函數中,讓回調函數延遲了50ms再被調用,因此這種模式在同步和異步狀態下都運行良好。只需保證每一個要經過yield關鍵字調用的函數都按照與之相同的模式編寫

  理解了函數中異步過程的運做方式,能夠將任務執行器稍做修改。當result.value是一個函數時,任務執行器會先執行這個函數再將結果傳入next()方法

function run(taskDef) {
    // 建立迭代器,讓它在別處可用
    let task = taskDef();
    // 啓動任務
    let result = task.next();
    // 遞歸使用函數來保持對 next() 的調用
    function step() {
        // 若是還有更多要作的
        if (!result.done) {
            if (typeof result.value === "function") {
                result.value(function(err, data) {
                    if (err) {
                        result = task.throw(err);
                        return;
                    }
                    result = task.next(data);
                    step();
                });
            } else {
                result = task.next(result.value);
                step();
            }
        }
    }
    // 開始處理過程
    step();
}

  經過===操做符檢査後,若是result.value是一個函數,會傳入一個回調函數做爲參數調用它,回調函數遵循Node.js有關執行錯誤的約定:全部可能的錯誤放在第一個參數(err)中,結果放在第二個參數中。若是傳入了err,意味着執行過程當中產生了錯誤,這時經過task.throw()正確輸出錯誤對象;若是沒有錯誤產生,data被傳入task.next()做爲結果儲存起來,並繼續執行step()。若是result.value不是一個函數,則直接將其傳入next()方法

  如今,這個新版的任務執行器已經能夠用於全部的異步任務了。在Node.js環境中,若是要從文件中讀取一些數據,須要在fs.readFile()外圍建立一個包裝器(wrapper),並返回一個與fetchData()相似的函數

let fs = require("fs");
    function readFile(filename) {
        return function(callback) {
            fs.readFile(filename, callback);
        };
}

  readFile()接受一個文件名做爲參數,返回一個能夠執行回調函數的函數。回調函數被直接傳入fs.readFile()方法,讀取完成後會執行它

run(function*() {
    let contents = yield readFile("config.json");
    doSomethingWith(contents);
    console.log("Done");
});

  在這段代碼中沒有任何回調變量,異步的readFile()操做卻正常執行,除了yield關鍵字外,其餘代碼與同步代碼徹底同樣,只不過函數執行的是異步操做。因此遵循相同的接口,能夠編寫一些讀起來像是同步代碼的異步邏輯

  固然,這些示例中使用的模式也有缺點,也就是不能百分百確認函數中返回的其餘函數必定是異步的。着眼當下,最重要的是能理解任務執行過程背後的理論知識

相關文章
相關標籤/搜索