如何遍歷一個陣列的元素?在 20 年前,當 JavaScript 出現時,你也許會這樣做:
1 2 3 |
for (var index = 0; index < myArray.length; index++) { console.log(myArray[index]); } |
自從 ES5 開始,你可以使用內建的 forEach
方法:
1 2 3 |
myArray.forEach(function (value) { console.log(value); }); |
程式碼更為精簡,但有一個小缺點:不能使用 break
語句來跳出迴圈,也不能使用 return
語句來從閉包函式中返回。
如果有 for-
這種語法來遍歷陣列就會方便很多。
那麼,使用 for-in
怎麼樣?
1 2 3 |
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 解決該問題的唯一辦法是引入新的迴圈遍歷語法。
這就是新的語法:
1 2 3 |
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
物件來對陣列元素去重:
1 2 |
// make a set from an array of words var uniqueWords = new Set(words); |
當得到一個 Set
物件後,你很可能會去遍歷該物件,這很簡單:
1 2 3 |
for (var word of uniqueWords) { console.log(word); } |
Map
物件由鍵值對構成,遍歷方式略有不同,你需要用兩個獨立的變數來分別接收鍵和值:
1 2 3 |
for (var [key, value] of phoneBookMap) { console.log(key + "'s phone number is: " + value); } |
到目前為止,你已經知道:JS 已經支援一些集合物件,而且後面將會支援更多。for-of
語法正是為這些集合物件而設計。
for-of
不能直接用來遍歷物件的屬性,如果你想遍歷物件的屬性,你可以使用 for-in
語句(for-in
就是用來幹這個的),或者使用下面的方式:
1 2 3 4 |
// 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
語句,你可以這樣做:
1 2 3 4 |
// 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
方法。下面是一個最簡單的迭代器物件:
1 2 3 4 5 6 7 8 |
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
迴圈體:
1 2 3 |
for (VAR of ITERABLE) { STATEMENTS } |
這只是一個語義化的實現,使用了一些底層方法和幾個臨時變數:
1 2 3 4 5 6 7 |
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 的討論。