我們如何遍歷數組中的元素?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”、“2”,此時很可能在無意之間進行字符串算數計算,例如:“2” + 1 == “21”,這給編碼過程帶來極大的不便。
- 作用於數組的for-in循環體除了遍歷數組元素外,還會遍歷自定義屬性。舉個例子,如果你的數組中有一個可枚舉屬性myArray.name,循環將額外執行一次,遍歷到名為“name”的索引。就連數組原型鏈上的屬性都能被訪問到。
- 最讓人震驚的是,在某些情況下,這段代碼可能按照隨機順序遍歷數組元素。
- 簡而言之,for-in是為普通對象設計的,你可以遍歷得到字符串類型的鍵,因此不適用於數組遍歷。
強大的for-of循環
還記得在《深入淺出ES6(一):ES6是什么》中我向你們承諾過的話么?ES6不會破壞你已經寫好的JS代碼。目前看來,成千上萬的Web網站依賴for-in循環,其中一些網站甚至將其用於數組遍歷。如果想通過修正for-in循環增加數組遍歷支持會讓這一切變得更加混亂,因此,標准委員會在ES6中增加了一種新的循環語法來解決目前的問題。
就像這樣:
for (var value of myArray) { console.log(value); }
是的,與之前的內建方法相比,這種循環方式看起來是否有些眼熟?那好,我們將要探究一下for-of循環的外表下隱藏着哪些強大的功能。現在,只需記住:
- 這是最簡潔、最直接的遍歷數組元素的語法
- 這個方法避開了for-in循環的所有缺陷
- 與forEach()不同的是,它可以正確響應break、continue和return語句
for-in循環用來遍歷對象屬性。
for-of循環用來遍歷數據—例如數組中的值。
但是,不僅如此!
for-of循環也可以遍歷其它的集合
for-of循環不僅支持數組,還支持大多數類數組對象,例如DOM NodeList對象。
for-of循環也支持字符串遍歷,它將字符串視為一系列的Unicode字符來進行遍歷:
for (var chr of "") { alert(chr); }
它同樣支持Map和Set對象遍歷。
對不起,你一定沒聽說過Map和Set對象。他們是ES6中新增的類型。我們將在后續的文章講解這兩個新的類型。如果你曾在其它語言中使用過Map和Set,你會發現ES6中的並無太大出入。
舉個例子,Set對象可以自動排除重復項:
// 基於單詞數組創建一個set對象 var uniqueWords = new Set(words);
生成Set對象后,你可以輕松遍歷它所包含的內容:
for (var word of uniqueWords) { console.log(word); }
Map對象稍有不同:內含的數據由鍵值對組成,所以你需要使用解構(destructuring)來將鍵值對拆解為兩個獨立的變量:
for (var [key, value] of phoneBookMap) { console.log(key + "'s phone number is: " + value); }
解構也是ES6的新特性,我們將在另一篇文章中講解。看來我應該記錄這些優秀的主題,未來有太多的新內容需要一一剖析。
現在,你只需記住:未來的JS可以使用一些新型的集合類,甚至會有更多的類型陸續誕生,而for-of就是為遍歷所有這些集合特別設計的循環語句。
for-of循環不支持普通對象,但如果你想迭代一個對象的屬性,你可以用for-in循環(這也是它的本職工作)或內建的Object.keys()方法:
// 向控制台輸出對象的可枚舉屬性 for (var key of Object.keys(someObject)) { console.log(key + ": " + someObject[key]); }
深入理解
“能工摹形,巧匠竊意。”——巴勃羅·畢卡索
ES6始終堅持這樣的宗旨:凡是新加入的特性,勢必已在其它語言中得到強有力的實用性證明。
舉個例子,新加入的for-of循環像極了C++、Java、C#以及Python中的循環語句。與它們一樣,這里的for-of循環支持語言和標准庫中提供的幾種不同的數據結構。它同樣也是這門語言中的一個擴展點(譯注:關於擴展點,建議參考 1. 淺析擴展點 2. What are extensions and extension points?)。
正如其它語言中的for/foreach語句一樣,for-of循環語句通過方法調用來遍歷各種集合。數組、Maps對象、Sets對象以及其它在我們討論的對象有一個共同點,它們都有一個迭代器方法。
你可以給任意類型的對象添加迭代器方法。
當你為對象添加myObject.toString()方法后,就可以將對象轉化為字符串,同樣地,當你向任意對象添加myObject[Symbol.iterator]()方法,就可以遍歷這個對象了。
舉個例子,假設你正在使用jQuery,盡管你非常鍾情於里面的.each()方法,但你還是想讓jQuery對象也支持for-of循環,你可以這樣做:
// 因為jQuery對象與數組相似 // 可以為其添加與數組一致的迭代器方法 jQuery.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator];
好的,我知道你在想什么,那個[Symbol.iterator]語法看起來很奇怪,這段代碼到底做了什么呢?這里通過Symbol處理了一下方法的名稱。標准委員會可以把這個方法命名為.iterator()方法,但是如果你的代碼中的對象可能也有一些.iterator()方法,這一定會讓你感到非常困惑。於是在ES6標准中使用symbol來作為方法名,而不是使用字符串。
你大概也猜到了,Symbols是ES6中的新類型,我們會在后續的文章中講解。現在,你需要記住,基於新標准,你可以定義一個全新的symbol,就像Symbol.iterator,如此一來可以保證不與任何已有代碼產生沖突。這樣做的代價是,這段代碼的語法看起來會略顯生硬,但是這微乎其微代價卻可以為你帶來如此多的新特性和新功能,並且你所做的這一切可以完美地向后兼容。
所有擁有[Symbol.iterator]()的對象被稱為可迭代的。在接下來的文章中你會發現,可迭代對象的概念幾乎貫穿於整門語言之中,不僅是for-of循環,還有Map和Set構造函數、解構賦值,以及新的展開操作符。
迭代器對象
現在,你將無須親自從零開始實現一個對象迭代器,我們會在下一篇文章詳細講解。為了幫助你理解本文,我們簡單了解一下迭代器(如果你跳過這一章,你將錯過非常精彩的技術細節)。
for-of循環首先調用集合的[Symbol.iterator]()方法,緊接着返回一個新的迭代器對象。迭代器對象可以是任意具有.next()方法的對象;for-of循環將重復調用這個方法,每次循環調用一次。舉個例子,這段代碼是我能想出來的最簡單的迭代器:
var zeroesForeverIterator = { [Symbol.iterator]: function () { return this; }, next: function () { return {done: false, value: 0}; } };
每一次調用.next()方法,它都返回相同的結果,返回給for-of循環的結果有兩種可能:(a) 我們尚未完成迭代;(b) 下一個值為0。這意味着(value of zeroesForeverIterator) {}將會是一個無限循環。當然,一般來說迭代器不會如此簡單。
這個迭代器的設計,以及它的.done和.value屬性,從表面上看與其它語言中的迭代器不太一樣。在Java中,迭代器有分離的.hasNext()和.next()方法。在Python中,他們只有一個.next() 方法,當沒有更多值時拋出StopIteration異常。但是所有這三種設計從根本上講都返回了相同的信息。
迭代器對象也可以實現可選的.return()和.throw(exc)方法。如果for-of循環過早退出會調用.return()方法,異常、break語句或return語句均可觸發過早退出。如果迭代器需要執行一些清潔或釋放資源的操作,可以在.return()方法中實現。大多數迭代器方法無須實現這一方法。.throw(exc)方法的使用場景就更特殊了:for-of循環永遠不會調用它。但是我們還是會在下一篇文章更詳細地講解它的作用。
現在我們已了解所有細節,可以寫一個簡單的for-of循環然后按照下面的方法調用重寫被迭代的對象。
首先是for-of循環:
for (VAR of ITERABLE) { 一些語句 }
然后是一個使用以下方法和少許臨時變量實現的與之前大致相當的示例,:
var $iterator = ITERABLE[Symbol.iterator](); var $result = $iterator.next(); while (!$result.done) { VAR = $result.value; 一些語句 $result = $iterator.next(); }
這段代碼沒有展示.return()方法是如何處理的,我們可以添加這部分代碼,但我認為這對於我們正在講解的內容來說過於復雜了。for-of循環用起來很簡單,但是其背后有着非常復雜的機制。
我何時可以開始使用這一新特性?
目前,對於for-of循環新特性,所有最新版本Firefox都(部分)支持(譯注:從FF 13開始陸續支持相關功能,FF 36 - FF 40基本支持大部分特性),在Chrome中可以通過訪問 chrome://flags 並啟用“實驗性JavaScript”來支持。微軟的Spartan瀏覽器支持,但是IE不支持。如果你想在web環境中使用這種新語法,同時需要支持IE和Safari,你可以使用Babel或Google的Traceur這些編譯器來將你的ES6代碼翻譯為Web友好的ES5代碼。
而在服務端,你不需要類似的編譯器,io.js中默認支持ES6新語法(部分),在Node中需要添加--harmony選項來啟用相關特性。