英文:Jason Orendorff
譯者:bubkoo的部落格(@問崖的崖)
網址:http://bubkoo.com/2015/06/15/es6-in-depth-iterators-and-the-for-of-loop/
如何遍歷一個陣列的元素?在 20 年前,當 JavaScript 出現時,你也許會這樣做:
for (var index = 0; index < myArray.length; index++) {
console.log(myArray[index]);
}
自從 ES5 開始,你可以使用內建的 forEach 方法:
myArray.forEach(function (value) {
console.log(value);
});
程式碼更為精簡,但有一個小缺點:不能使用 break 陳述句來跳出迴圈,也不能使用 return 陳述句來從閉包函式中傳回。
如果有 for- 這種語法來遍歷陣列就會方便很多。
那麼,使用 for-in 怎麼樣?
for (var index in myArray) { // 實際程式碼中不要這麼做
console.log(myArray[index]);
}
這樣不好,因為:
上面程式碼中的 index 變數將會是 “0”、”1″、”3″ 等這樣的字串,而並不是數值型別。如果你使用字串的 index 去參與某些運算(”2″ + 1 == “21”),運算結果可能會不符合預期。
不僅陣列本身的元素將被遍歷到,那些由使用者新增的附加(expando)元素也將被遍歷到,例如某陣列有這樣一個屬性 myArray.name,那麼在某次迴圈中將會出現 index=”name” 的情況。而且,甚至連陣列原型鏈上的屬性也可能被遍歷到。
最不可思議的是,在某些情況下,上面程式碼將會以任意順序去遍歷陣列元素。
簡單來說,for-in 設計的目的是用於遍歷包含鍵值對的物件,對陣列並不是那麼友好。
強大的 for-of 迴圈
記得上次我提到過,ES6 並不會影響現有 JS 程式碼的正常執行,已經有成千上萬的 Web 應用都依賴於 for-in 的特性,甚至也依賴 for-in 用於陣列的特性,所以從來就沒有人提出“改善”現有 for-in 語法來修複上述問題。ES6 解決該問題的唯一辦法是引入新的迴圈遍歷語法。
這就是新的語法:
for (var value of myArray) {
console.log(value);
}
透過介紹上面的 for-in 語法,這個語法看起來並不是那麼令人印象深刻。後面我們將詳細介紹for-of 的奇妙之處,現在你只需要知道:
- 這是遍歷陣列最簡單直接的方法
- 避免了所有 for–in 語法存在的坑
- 與 forEach() 不同的是,它支援 break、continue 和 return 陳述句。
for–in 用於遍歷物件的屬性。
for-of 用於遍歷資料 — 就像陣列中的元素。
然而,這還不是 for-of 的所有特性,下麵還有更精彩的部分。
支援 for-of 的其他集合
for-of 不僅僅是為陣列設計,還可以用於類陣列的物件,比如 DOM 物件的集合 NodeList。
也可以用於遍歷字串,它將字串看成是 Unicode 字元的集合:
它還適用於 Map 和 Set 物件。
也許你從未聽說過 Map 和 Set 物件,因為它們是 ES6 中的新物件,後面將有單獨的文章去詳細介紹它們。如果你在其他語言中使用過這兩個物件,那就簡單多了。
例如,可以用一個 Set 物件來對陣列元素去重:
// make a set from an array of words
var uniqueWords = new Set(words);
當得到一個 Set 物件後,你很可能會去遍歷該物件,這很簡單:
for (var word of uniqueWords) {
console.log(word);
}
Map 物件由鍵值對構成,遍歷方式略有不同,你需要用兩個獨立的變數來分別接收鍵和值:
for (var [key, value] of phoneBookMap) {
console.log(key + “‘s phone number is: ” + value);
}
到目前為止,你已經知道:JS 已經支援一些集合物件,而且後面將會支援更多。for-of 語法正是為這些集合物件而設計。
for-of 不能直接用來遍歷物件的屬性,如果你想遍歷物件的屬性,你可以使用 for-in 陳述句(for-in 就是用來乾這個的),或者使用下麵的方式:
// dump an object’s own enumerable properties to the console
for (var key of Object.keys(someObject)) {
console.log(key + “: ” + someObject[key]);
}
內部原理
“好的藝術家複製,偉大的藝術家偷竊。” — 巴勃羅·畢加索
被新增到 ES6 中的那些新特性並不是無章可循,大多數特性都已經被使用在其他語言中,而且事實也證明這些特性很有用。
就拿 for-of 陳述句來說,在 C++、JAVA、C# 和 Python 中都存在類似的迴圈陳述句,並且用於遍歷這門語言和其標準庫中的各種資料結構。
與其他語言中的 for 和 foreach 陳述句一樣,for-of 要求被遍歷的物件實現特定的方法。所有的 Array、Map 和 Set 物件都有一個共性,那就是他們都實現了一個迭代器(iterator)方法。
那麼,只要你願意,對其他任何物件你都可以實現一個迭代器方法。
這就像你可以為一個物件實現一個 myObject.toString() 方法,來告知 JS 引擎如何將一個物件轉換為字串;你也可以為任何物件實現一個 myObject[Symbol.iterator]() 方法,來告知 JS 引擎如何去遍歷該物件。
例如,如果你正在使用 jQuery,並且非常喜歡用它的 each() 方法,現在你想使所有的 jQuery 物件都支援 for-of 陳述句,你可以這樣做:
// Since jQuery objects are array-like,
// give them the same iterator method Arrays have
jQuery.prototype[Symbol.iterator] =
Array.prototype[Symbol.iterator];
你也許在想,為什麼 [Symbol.iterator] 語法看起來如此奇怪?這句話到底是什麼意思?問題的關鍵在於方法名,ES 標準委員會完全可以將該方法命名為 iterator(),但是,現有物件中可能已經存在名為“iterator”的方法,這將導致程式碼混亂,違背了最大相容性原則。所以,標準委員會引入了 Symbol,而不僅僅是一個字串,來作為方法名。
Symbol 也是 ES6 的新特性,後面將會有單獨的文章來介紹。現在你只需要知道標準委員會引入全新的 Symbol,比如 Symbol.iterator,是為了不與之前的程式碼衝突。唯一不足就是語法有點奇怪,但對於這個強大的新特性和完美的後向相容來說,這個就顯得微不足道了。
一個擁有 [Symbol.iterator]() 方法的物件被認為是可遍歷的(iterable)。在後面的文章中,我們將看到“可遍歷物件”的概念貫穿在整個語言中,不僅在 for-of 陳述句中,而且在 Map和 Set 的建構式和析構(Destructuring)函式中,以及新的擴充套件運運算元中,都將涉及到。
迭代器物件
通常我們不會完完全全從頭開始去實現一個迭代器(Iterator)物件,下一篇文章將告訴你為什麼。但為了完整起見,讓我們來看看一個迭代器物件具體是什麼樣的。(如果你跳過了本節,你將會錯失某些技術細節。)
就拿 for-of 陳述句來說,它首先呼叫被遍歷集合物件的 [Symbol.iterator]() 方法,該方法傳回一個迭代器物件,迭代器物件可以是擁有 .next 方法的任何物件;然後,在 for-of 的每次迴圈中,都將呼叫該迭代器物件上的 .next 方法。下麵是一個最簡單的迭代器物件:
var zeroesForeverIterator = {
[Symbol.iterator]: function () {
return this;
},
next: function () {
return {done: false, value: 0};
}
};
在上面程式碼中,每次呼叫 .next() 方法時都傳回了同一個結果,該結果一方面告知 for-of陳述句迴圈遍歷還沒有結束,另一方面告知 for-of 陳述句本次迴圈的值為 0。這意味著 for (value of zeroesForeverIterator) {} 是一個死迴圈。當然,一個典型的迭代器不會如此簡單。
ES6 的迭代器透過 .done 和 .value 這兩個屬性來標識每次的遍歷結果,這就是迭代器的設計原理,這與其他語言中的迭代器有所不同。在 Java 中,迭代器物件要分別使用 .hasNext()和 .next() 兩個方法。在 Python 中,迭代器物件只有一個 .next() 方法,當沒有可遍歷的元素時將丟擲一個 StopIteration 異常。但從根本上說,這三種設計都傳回了相同的資訊。
迭代器物件可以還可以選擇性地實現 .return() 和 .throw(exc) 這兩個方法。如果由於異常或使用 break 和 return 運運算元導致迴圈提早退出,那麼迭代器的 .return() 方法將被呼叫,可以透過實現 .return() 方法來釋放迭代器物件所佔用的資源,但大多數迭代器都不需要實現這個方法。throw(exc) 更是一個特例:在遍歷過程中該方法永遠都不會被呼叫,關於這個方法,我會在下一篇文章詳細介紹。
現在我們知道了 for-of 的所有細節,那麼我們可以簡單地重寫該陳述句。
首先是 for-of 迴圈體:
for (VAR of ITERABLE) {
STATEMENTS
}
這隻是一個語意化的實現,使用了一些底層方法和幾個臨時變數:
var $iterator = ITERABLE[Symbol.iterator]();
var $result = $iterator.next();
while (!$result.done) {
VAR = $result.value;
STATEMENTS
$result = $iterator.next();
}
上面程式碼並沒有涉及到如何呼叫 .return() 方法,我們可以新增相應的處理,但我認為這樣會影響我們對內部原理的理解。for-of 陳述句使用起來非常簡單,但在其內部有非常多的細節。
相容性
目前,所有 Firefox 的 Release 版本都已經支援 for-of 陳述句。Chrome 預設禁用了該陳述句,你可以在位址列輸入 chrome://flags 進入設定頁面,然後勾選其中的 “Experimental JavaScript” 選項。微軟的 Spartan 瀏覽器也支援該陳述句,但是 IE 不支援。如果你想在 Web 開發中使用該陳述句,而且需要相容 IE 和 Safari 瀏覽器,你可以使用 Babel 或 Google 的 Traceur 這類編譯器,來將 ES6 程式碼轉換為 Web 友好的 ES5 程式碼。
對於伺服器端,我們不需要任何編譯器 — 可以在 io.js 中直接使用該陳述句,或者在 NodeJS 啟動時使用 –harmony 啟動選項。
{done: true}
到此,今天的話題已經結束,但對於 for-of 的話題還沒有結束。
在 ES6 中還有一個新物件,該物件可以與 for-of 陳述句完美地結合使用,今天我並沒有提及該物件,因為這是下篇文章我們討論的主題,我認為這個新物件是 ES6 中最大的特性。如果你還沒有在 Python 或 C# 中接觸過該物件,你會認為這太奇妙了,但這是編寫一個迭代器的最簡單的方法,而且它對程式碼重構非常有用,它還可能改變我們處理非同步程式碼的方式。所以,接著關註我的下篇關於 Generator 的討論。
本系列