如何遍歷一個數組的元素?在 20 年前,當 JavaScript 出現時,你也許會這樣做:
for (var index = 0; index < myArray.length; index++) { console.log(myArray[index]); } for (var index = 0; index < myArray.length; index++) { console.log(myArray[index]); }
自從 ES5 開始,你可以使用內置的 forEach 方法:
JavaScript myArray.forEach(function (value) { console.log(value); }); myArray.forEach(function (value) { console.log(value); });
代碼更為精簡,但有一個小缺點:不能使用 break 語句來跳出循環,也不能使用 return 語句來從閉包函數中返回。
如果有 for- 這種語法來遍歷數組就會方便很多。
那麼,使用 for-in 怎麼樣?
for (var index in myArray) { // 實際代碼中不要這麼做 console.log(myArray[index]); } 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 (var value of myArray) { console.log(value); }
通過介紹上面的 for-in 語法,這個語法看起來並不是那麼令人印象深刻。後面我們將詳細介紹for-of 的奇妙之處,現在你只需要知道:
for–in 用於遍歷對象的屬性。
for-of 用於遍歷數據 — 就像數組中的元素。
然而,這還不是 for-of 的所有特性,下面還有更精彩的部分。
支持 for-of 的其他集合
for-of 不僅僅是為數組設計,還可以用於類數組的對象,比如 DOM 對象的集合 NodeList。
也可以用於遍歷字符串,它將字符串看成是 Unicode 字符的集合:
它還適用於 Map 和 Set 對象。
也許你從未聽說過 Map 和 Set 對象,因為它們是 ES6 中的新對象,後面將有單獨的文章去詳細介紹它們。如果你在其他語言中使用過這兩個對象,那就簡單多了。
例如,可以用一個 Set 對象來對數組元素去重:
JavaScript // make a set from an array of words var uniqueWords = new Set(words); // make a set from an array of words var uniqueWords = new Set(words);
當得到一個 Set 對象後,你很可能會去遍歷該對象,這很簡單:
for (var word of uniqueWords) { console.log(word); } for (var word of uniqueWords) { console.log(word); }
Map 對象由鍵值對構成,遍歷方式略有不同,你需要用兩個獨立的變量來分別接收鍵和值:
for (var [key, value] of phoneBookMap) { console.log(key + "'s phone number is: " + value); } 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]); } // 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]; // 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}; } }; 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 } for (VAR of ITERABLE) { STATEMENTS }
這只是一個語義化的實現,使用了一些底層方法和幾個臨時變量:
var $iterator = ITERABLE[Symbol.iterator](); var $result = $iterator.next(); while (!$result.done) { VAR = $result.value; STATEMENTS $result = $iterator.next(); } 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}