迭代器是ES6的一個重要組成部分,在ES6中,已經默認爲許多內建類型提供了內建迭代器,只有當這些內建迭代器沒法實現目標時才須要本身建立。一般來講當定義本身的對象和類時纔會遇到這種狀況,不然,徹底能夠依靠內建的迭代器完成工做,而最常使用的多是集合的那些迭代器編程
一、集合對象迭代器json
在ES6中有3種類型的集合對象:數組、Map集合與Set集合數組
爲了更好地訪問對象中的內容,這3種對象都內建瞭如下三種迭代器瀏覽器
(1)entries() 返回一個迭代器,其值爲多個鍵值對app
(2)values() 返回一個迭代器,其值爲集合的值異步
(3)keys() 返回一個迭代器,其值爲集合中的全部鍵名異步編程
調用以上3個方法均可以訪問集合的迭代器:函數
entries()迭代器:每次調用next()方法時,entries()迭代器都會返回一個數組,數組中的兩個元素分別表示集合中每一個元素的鍵與值。若是被遍歷的對象是數組,則第一個元素是數字類型的索引;若是是Set集合,則第一個元素與第二個元素都是值(Set集合中的值被同時做爲鍵與值使用);若是是Map集合,則第一個元素爲鍵名,第二個元素爲值。fetch
values()迭代器:調用values()迭代器時會返回集合中所存的全部值ui
keys()迭代器:keys()迭代器會返回集合中存在的每個鍵。若是遍歷的是數組,則會返回數字類型的鍵,數組自己的其餘屬性不會被返回;若是是Set集合,因爲鍵與值是相同的,所以keys()和values()返回的也是相同的迭代器;若是是Map集合,則keys()迭代器會返回每一個獨立的鍵
不一樣集合類型的默認迭代器:每一個集合類型都有一個默認的迭代器,在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集合,則按照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之間打印出4個空行。
所幸,ES6的目標是全面支持Unicode,而且咱們能夠經過改變字符串的默認迭代器來解決這個問題,使其操做字符而不是編碼單元。如今,修改前一個示例中字符串的默認迭代器,讓for-of循環輸出正確的內容
var message = "A 𠮷 B" ; for (let c of message) { console.log(c); }
這個執行結果更符合預期,經過循環語句能夠直接操做字符併成功打印出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語句返回值
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關鍵字外,其餘代碼與同步代碼徹底同樣,只不過函數執行的是異步操做。因此遵循相同的接口,能夠編寫一些讀起來像是同步代碼的異步邏輯
固然,這些示例中使用的模式也有缺點,也就是不能百分百確認函數中返回的其餘函數必定是異步的。着眼當下,最重要的是能理解任務執行過程背後的理論知識