深入解析 ES6:Iterator 和 for-of 迴圈

發表於2015-07-24

如何遍歷一個陣列的元素?在 20 年前,當 JavaScript 出現時,你也許會這樣做:

自從 ES5 開始,你可以使用內建的 forEach 方法:

程式碼更為精簡,但有一個小缺點:不能使用 break 語句來跳出迴圈,也不能使用 return 語句來從閉包函式中返回。

如果有 for- 這種語法來遍歷陣列就會方便很多。

那麼,使用 for-in 怎麼樣?

這樣不好,因為:

  • 上面程式碼中的 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-in 語法,這個語法看起來並不是那麼令人印象深刻。後面我們將詳細介紹for-of 的奇妙之處,現在你只需要知道:

  • 這是遍歷陣列最簡單直接的方法
  • 避免了所有 for–in 語法存在的坑
  • 與 forEach() 不同的是,它支援 breakcontinue 和 return 語句。

for–in 用於遍歷物件的屬性。

for-of 用於遍歷資料 — 就像陣列中的元素。

然而,這還不是 for-of 的所有特性,下面還有更精彩的部分。

支援 for-of 的其他集合

for-of 不僅僅是為陣列設計,還可以用於類陣列的物件,比如 DOM 物件的集合 NodeList

也可以用於遍歷字串,它將字串看成是 Unicode 字元的集合:

它還適用於 Map 和 Set 物件。

也許你從未聽說過 Map 和 Set 物件,因為它們是 ES6 中的新物件,後面將有單獨的文章去詳細介紹它們。如果你在其他語言中使用過這兩個物件,那就簡單多了。

例如,可以用一個 Set 物件來對陣列元素去重:

當得到一個 Set 物件後,你很可能會去遍歷該物件,這很簡單:

Map 物件由鍵值對構成,遍歷方式略有不同,你需要用兩個獨立的變數來分別接收鍵和值:

到目前為止,你已經知道:JS 已經支援一些集合物件,而且後面將會支援更多。for-of 語法正是為這些集合物件而設計。

for-of 不能直接用來遍歷物件的屬性,如果你想遍歷物件的屬性,你可以使用 for-in 語句(for-in 就是用來幹這個的),或者使用下面的方式:

內部原理

“好的藝術家複製,偉大的藝術家偷竊。” — 巴勃羅·畢加索

被新增到 ES6 中的那些新特性並不是無章可循,大多數特性都已經被使用在其他語言中,而且事實也證明這些特性很有用。

就拿 for-of 語句來說,在 C++、JAVA、C# 和 Python 中都存在類似的迴圈語句,並且用於遍歷這門語言和其標準庫中的各種資料結構。

與其他語言中的 for 和 foreach 語句一樣,for-of 要求被遍歷的物件實現特定的方法。所有的 ArrayMap 和 Set 物件都有一個共性,那就是他們都實現了一個迭代器(iterator)方法。

那麼,只要你願意,對其他任何物件你都可以實現一個迭代器方法。

這就像你可以為一個物件實現一個 myObject.toString() 方法,來告知 JS 引擎如何將一個物件轉換為字串;你也可以為任何物件實現一個 myObject[Symbol.iterator]() 方法,來告知 JS 引擎如何去遍歷該物件。

例如,如果你正在使用 jQuery,並且非常喜歡用它的 each() 方法,現在你想使所有的 jQuery 物件都支援 for-of 語句,你可以這樣做:

你也許在想,為什麼 [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 方法。下面是一個最簡單的迭代器物件:

在上面程式碼中,每次呼叫 .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 迴圈體:

這只是一個語義化的實現,使用了一些底層方法和幾個臨時變數:

上面程式碼並沒有涉及到如何呼叫 .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 的討論。

相關文章