深入解讀JavaScript中的Iterator和for-of循環(huán)
如何遍歷一個(gè)數(shù)組的元素?在 20 年前,當(dāng) JavaScript 出現(xiàn)時(shí),你也許會(huì)這樣做:
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 開始,你可以使用內(nèi)置的 forEach 方法:
JavaScript myArray.forEach(function (value) { console.log(value); }); myArray.forEach(function (value) { console.log(value); });
代碼更為精簡,但有一個(gè)小缺點(diǎn):不能使用 break 語句來跳出循環(huán),也不能使用 return 語句來從閉包函數(shù)中返回。
如果有 for- 這種語法來遍歷數(shù)組就會(huì)方便很多。
那么,使用 for-in 怎么樣?
for (var index in myArray) { // 實(shí)際代碼中不要這么做 console.log(myArray[index]); } for (var index in myArray) { // 實(shí)際代碼中不要這么做 console.log(myArray[index]); }
這樣不好,因?yàn)椋?/p>
上面代碼中的 index 變量將會(huì)是 "0"、"1"、"3" 等這樣的字符串,而并不是數(shù)值類型。如果你使用字符串的 index 去參與某些運(yùn)算("2" + 1 == "21"),運(yùn)算結(jié)果可能會(huì)不符合預(yù)期。
不僅數(shù)組本身的元素將被遍歷到,那些由用戶添加的附加(expando)元素也將被遍歷到,例如某數(shù)組有這樣一個(gè)屬性 myArray.name,那么在某次循環(huán)中將會(huì)出現(xiàn) index="name" 的情況。而且,甚至連數(shù)組原型鏈上的屬性也可能被遍歷到。
最不可思議的是,在某些情況下,上面代碼將會(huì)以任意順序去遍歷數(shù)組元素。
簡單來說,for-in 設(shè)計(jì)的目的是用于遍歷包含鍵值對的對象,對數(shù)組并不是那么友好。
強(qiáng)大的 for-of 循環(huán)
記得上次我提到過,ES6 并不會(huì)影響現(xiàn)有 JS 代碼的正常運(yùn)行,已經(jīng)有成千上萬的 Web 應(yīng)用都依賴于 for-in 的特性,甚至也依賴 for-in 用于數(shù)組的特性,所以從來就沒有人提出“改善”現(xiàn)有 for-in 語法來修復(fù)上述問題。ES6 解決該問題的唯一辦法是引入新的循環(huán)遍歷語法。
這就是新的語法:
for (var value of myArray) { console.log(value); } for (var value of myArray) { console.log(value); }
通過介紹上面的 for-in 語法,這個(gè)語法看起來并不是那么令人印象深刻。后面我們將詳細(xì)介紹for-of 的奇妙之處,現(xiàn)在你只需要知道:
- 這是遍歷數(shù)組最簡單直接的方法
- 避免了所有 for–in 語法存在的坑
- 與 forEach() 不同的是,它支持 break、continue 和 return 語句。
for–in 用于遍歷對象的屬性。
for-of 用于遍歷數(shù)據(jù) — 就像數(shù)組中的元素。
然而,這還不是 for-of 的所有特性,下面還有更精彩的部分。
支持 for-of 的其他集合
for-of 不僅僅是為數(shù)組設(shè)計(jì),還可以用于類數(shù)組的對象,比如 DOM 對象的集合 NodeList。
也可以用于遍歷字符串,它將字符串看成是 Unicode 字符的集合:
它還適用于 Map 和 Set 對象。
也許你從未聽說過 Map 和 Set 對象,因?yàn)樗鼈兪?ES6 中的新對象,后面將有單獨(dú)的文章去詳細(xì)介紹它們。如果你在其他語言中使用過這兩個(gè)對象,那就簡單多了。
例如,可以用一個(gè) Set 對象來對數(shù)組元素去重:
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);
當(dāng)?shù)玫揭粋€(gè) Set 對象后,你很可能會(huì)去遍歷該對象,這很簡單:
for (var word of uniqueWords) { console.log(word); } for (var word of uniqueWords) { console.log(word); }
Map 對象由鍵值對構(gòu)成,遍歷方式略有不同,你需要用兩個(gè)獨(dú)立的變量來分別接收鍵和值:
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); }
到目前為止,你已經(jīng)知道:JS 已經(jīng)支持一些集合對象,而且后面將會(huì)支持更多。for-of 語法正是為這些集合對象而設(shè)計(jì)。
for-of 不能直接用來遍歷對象的屬性,如果你想遍歷對象的屬性,你可以使用 for-in 語句(for-in 就是用來干這個(gè)的),或者使用下面的方式:
// 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]); }
內(nèi)部原理
“好的藝術(shù)家復(fù)制,偉大的藝術(shù)家偷竊。” — 巴勃羅·畢加索
被添加到 ES6 中的那些新特性并不是無章可循,大多數(shù)特性都已經(jīng)被使用在其他語言中,而且事實(shí)也證明這些特性很有用。
就拿 for-of 語句來說,在 C++、JAVA、C# 和 Python 中都存在類似的循環(huán)語句,并且用于遍歷這門語言和其標(biāo)準(zhǔn)庫中的各種數(shù)據(jù)結(jié)構(gòu)。
與其他語言中的 for 和 foreach 語句一樣,for-of 要求被遍歷的對象實(shí)現(xiàn)特定的方法。所有的 Array、Map 和 Set 對象都有一個(gè)共性,那就是他們都實(shí)現(xiàn)了一個(gè)迭代器(iterator)方法。
那么,只要你愿意,對其他任何對象你都可以實(shí)現(xiàn)一個(gè)迭代器方法。
這就像你可以為一個(gè)對象實(shí)現(xiàn)一個(gè) myObject.toString() 方法,來告知 JS 引擎如何將一個(gè)對象轉(zhuǎn)換為字符串;你也可以為任何對象實(shí)現(xiàn)一個(gè) myObject[Symbol.iterator]() 方法,來告知 JS 引擎如何去遍歷該對象。
例如,如果你正在使用 jQuery,并且非常喜歡用它的 each() 方法,現(xiàn)在你想使所有的 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] 語法看起來如此奇怪?這句話到底是什么意思?問題的關(guān)鍵在于方法名,ES 標(biāo)準(zhǔn)委員會(huì)完全可以將該方法命名為 iterator(),但是,現(xiàn)有對象中可能已經(jīng)存在名為“iterator”的方法,這將導(dǎo)致代碼混亂,違背了最大兼容性原則。所以,標(biāo)準(zhǔn)委員會(huì)引入了 Symbol,而不僅僅是一個(gè)字符串,來作為方法名。
Symbol 也是 ES6 的新特性,后面將會(huì)有單獨(dú)的文章來介紹?,F(xiàn)在你只需要知道標(biāo)準(zhǔn)委員會(huì)引入全新的 Symbol,比如 Symbol.iterator,是為了不與之前的代碼沖突。唯一不足就是語法有點(diǎn)奇怪,但對于這個(gè)強(qiáng)大的新特性和完美的后向兼容來說,這個(gè)就顯得微不足道了。
一個(gè)擁有 [Symbol.iterator]() 方法的對象被認(rèn)為是可遍歷的(iterable)。在后面的文章中,我們將看到“可遍歷對象”的概念貫穿在整個(gè)語言中,不僅在 for-of 語句中,而且在 Map和 Set 的構(gòu)造函數(shù)和析構(gòu)(Destructuring)函數(shù)中,以及新的擴(kuò)展操作符中,都將涉及到。
迭代器對象
通常我們不會(huì)完完全全從頭開始去實(shí)現(xiàn)一個(gè)迭代器(Iterator)對象,下一篇文章將告訴你為什么。但為了完整起見,讓我們來看看一個(gè)迭代器對象具體是什么樣的。(如果你跳過了本節(jié),你將會(huì)錯(cuò)失某些技術(shù)細(xì)節(jié)。)
就拿 for-of 語句來說,它首先調(diào)用被遍歷集合對象的 [Symbol.iterator]() 方法,該方法返回一個(gè)迭代器對象,迭代器對象可以是擁有 .next 方法的任何對象;然后,在 for-of 的每次循環(huán)中,都將調(diào)用該迭代器對象上的 .next 方法。下面是一個(gè)最簡單的迭代器對象:
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}; } };
在上面代碼中,每次調(diào)用 .next() 方法時(shí)都返回了同一個(gè)結(jié)果,該結(jié)果一方面告知 for-of語句循環(huán)遍歷還沒有結(jié)束,另一方面告知 for-of 語句本次循環(huán)的值為 0。這意味著 for (value of zeroesForeverIterator) {} 是一個(gè)死循環(huán)。當(dāng)然,一個(gè)典型的迭代器不會(huì)如此簡單。
ES6 的迭代器通過 .done 和 .value 這兩個(gè)屬性來標(biāo)識(shí)每次的遍歷結(jié)果,這就是迭代器的設(shè)計(jì)原理,這與其他語言中的迭代器有所不同。在 Java 中,迭代器對象要分別使用 .hasNext()和 .next() 兩個(gè)方法。在 Python 中,迭代器對象只有一個(gè) .next() 方法,當(dāng)沒有可遍歷的元素時(shí)將拋出一個(gè) StopIteration 異常。但從根本上說,這三種設(shè)計(jì)都返回了相同的信息。
迭代器對象可以還可以選擇性地實(shí)現(xiàn) .return() 和 .throw(exc) 這兩個(gè)方法。如果由于異?;蚴褂?break 和 return 操作符導(dǎo)致循環(huán)提早退出,那么迭代器的 .return() 方法將被調(diào)用,可以通過實(shí)現(xiàn) .return() 方法來釋放迭代器對象所占用的資源,但大多數(shù)迭代器都不需要實(shí)現(xiàn)這個(gè)方法。throw(exc) 更是一個(gè)特例:在遍歷過程中該方法永遠(yuǎn)都不會(huì)被調(diào)用,關(guān)于這個(gè)方法,我會(huì)在下一篇文章詳細(xì)介紹。
現(xiàn)在我們知道了 for-of 的所有細(xì)節(jié),那么我們可以簡單地重寫該語句。
首先是 for-of 循環(huán)體:
for (VAR of ITERABLE) { STATEMENTS } for (VAR of ITERABLE) { STATEMENTS }
這只是一個(gè)語義化的實(shí)現(xiàn),使用了一些底層方法和幾個(gè)臨時(shí)變量:
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(); }
上面代碼并沒有涉及到如何調(diào)用 .return() 方法,我們可以添加相應(yīng)的處理,但我認(rèn)為這樣會(huì)影響我們對內(nèi)部原理的理解。for-of 語句使用起來非常簡單,但在其內(nèi)部有非常多的細(xì)節(jié)。
兼容性
目前,所有 Firefox 的 Release 版本都已經(jīng)支持 for-of 語句。Chrome 默認(rèn)禁用了該語句,你可以在地址欄輸入 chrome://flags 進(jìn)入設(shè)置頁面,然后勾選其中的 “Experimental JavaScript” 選項(xiàng)。微軟的 Spartan 瀏覽器也支持該語句,但是 IE 不支持。如果你想在 Web 開發(fā)中使用該語句,而且需要兼容 IE 和 Safari 瀏覽器,你可以使用 Babel 或 Google 的 Traceur 這類編譯器,來將 ES6 代碼轉(zhuǎn)換為 Web 友好的 ES5 代碼。
對于服務(wù)器端,我們不需要任何編譯器 — 可以在 io.js 中直接使用該語句,或者在 NodeJS 啟動(dòng)時(shí)使用 --harmony 啟動(dòng)選項(xiàng)。
{done: true}
相關(guān)文章
關(guān)于JS字符串函數(shù)String.replace()
本篇介紹關(guān)于JS字符串函數(shù)String.replace(),有需要的朋友參考一下。2013-04-04淺談js和css內(nèi)聯(lián)外聯(lián)注意事項(xiàng)
下面小編就為大家?guī)硪黄獪\談js和css內(nèi)聯(lián)外聯(lián)注意事項(xiàng)。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2016-06-06