深入淺出ES6(二):迭代器和for-of循環(huán)

本文為前端之巔原創(chuàng)文章,作者為Jason Orendorff,譯者為L(zhǎng)enville。未經(jīng)許可,拒絕任何形式的轉(zhuǎn)載。

我們?nèi)绾伪闅v數(shù)組中的元素?20年前JavaScript剛萌生時(shí),你可能這樣實(shí)現(xiàn)數(shù)組遍歷:

for (var index = 0; index < myArray.length; index++) {
  console.log(myArray[index]);
}

自ES5正式發(fā)布后,你可以使用內(nèi)建的forEach方法來遍歷數(shù)組:

myArray.forEach(function (value) {
console.log(value);
});

這段代碼看起來更加簡(jiǎn)潔,但這種方法也有一個(gè)小缺陷:你不能使用break語(yǔ)句中斷循環(huán),也不能使用return語(yǔ)句返回到外層函數(shù)。

當(dāng)然,如果只用for循環(huán)的語(yǔ)法來遍歷數(shù)組元素也很不錯(cuò)。

那么,你一定想嘗試一下for-in循環(huán):

for (var index in myArray) { // 千萬別這樣做
  console.log(myArray[index]);
}

這絕對(duì)是一個(gè)糟糕的選擇,為什么呢?

  • 在這段代碼中,賦給index的值不是實(shí)際的數(shù)字,而是字符串“0”、“1”、“2”,此時(shí)很可能在無意之間進(jìn)行字符串算數(shù)計(jì)算,例如:“2” + 1 == “21”,這給編碼過程帶來極大的不便。
  • 作用于數(shù)組的for-in循環(huán)體除了遍歷數(shù)組元素外,還會(huì)遍歷自定義屬性。舉個(gè)例子,如果你的數(shù)組中有一個(gè)可枚舉屬性myArray.name,循環(huán)將額外執(zhí)行一次,遍歷到名為“name”的索引。就連數(shù)組原型鏈上的屬性都能被訪問到。
  • 最讓人震驚的是,在某些情況下,這段代碼可能按照隨機(jī)順序遍歷數(shù)組元素。
  • 簡(jiǎn)而言之,for-in是為普通對(duì)象設(shè)計(jì)的,你可以遍歷得到字符串類型的鍵,因此不適用于數(shù)組遍歷。

強(qiáng)大的for-of循環(huán)

還記得在《深入淺出ES6(一):ES6是什么》中我向你們承諾過的話么?ES6不會(huì)破壞你已經(jīng)寫好的JS代碼。目前看來,成千上萬的Web網(wǎng)站依賴for-in循環(huán),其中一些網(wǎng)站甚至將其用于數(shù)組遍歷。如果想通過修正for-in循環(huán)增加數(shù)組遍歷支持會(huì)讓這一切變得更加混亂,因此,標(biāo)準(zhǔn)委員會(huì)在ES6中增加了一種新的循環(huán)語(yǔ)法來解決目前的問題。

就像這樣:

for (var value of myArray) {
  console.log(value);
}

是的,與之前的內(nèi)建方法相比,這種循環(huán)方式看起來是否有些眼熟?那好,我們將要探究一下for-of循環(huán)的外表下隱藏著哪些強(qiáng)大的功能。現(xiàn)在,只需記住:

  • 這是最簡(jiǎn)潔、最直接的遍歷數(shù)組元素的語(yǔ)法
  • 這個(gè)方法避開了for-in循環(huán)的所有缺陷
  • 與forEach()不同的是,它可以正確響應(yīng)break、continue和return語(yǔ)句

for-in循環(huán)用來遍歷對(duì)象屬性。

for-of循環(huán)用來遍歷數(shù)據(jù)—例如數(shù)組中的值。

但是,不僅如此!

for-of循環(huán)也可以遍歷其它的集合

for-of循環(huán)不僅支持?jǐn)?shù)組,還支持大多數(shù)類數(shù)組對(duì)象,例如DOM NodeList對(duì)象

for-of循環(huán)也支持字符串遍歷,它將字符串視為一系列的Unicode字符來進(jìn)行遍歷:

for (var chr of "") {
  alert(chr);
}

它同樣支持Map和Set對(duì)象遍歷。

對(duì)不起,你一定沒聽說過Map和Set對(duì)象。他們是ES6中新增的類型。我們將在后續(xù)的文章講解這兩個(gè)新的類型。如果你曾在其它語(yǔ)言中使用過Map和Set,你會(huì)發(fā)現(xiàn)ES6中的并無太大出入。

舉個(gè)例子,Set對(duì)象可以自動(dòng)排除重復(fù)項(xiàng):

// 基于單詞數(shù)組創(chuàng)建一個(gè)set對(duì)象
var uniqueWords = new Set(words);

生成Set對(duì)象后,你可以輕松遍歷它所包含的內(nèi)容:

for (var word of uniqueWords) {
   console.log(word);
}

Map對(duì)象稍有不同:內(nèi)含的數(shù)據(jù)由鍵值對(duì)組成,所以你需要使用解構(gòu)(destructuring)來將鍵值對(duì)拆解為兩個(gè)獨(dú)立的變量:

for (var [key, value] of phoneBookMap) {
   console.log(key + "'s phone number is: " + value);
}

解構(gòu)也是ES6的新特性,我們將在另一篇文章中講解。看來我應(yīng)該記錄這些優(yōu)秀的主題,未來有太多的新內(nèi)容需要一一剖析。

現(xiàn)在,你只需記住:未來的JS可以使用一些新型的集合類,甚至?xí)懈嗟念愋完懤m(xù)誕生,而for-of就是為遍歷所有這些集合特別設(shè)計(jì)的循環(huán)語(yǔ)句。

for-of循環(huán)不支持普通對(duì)象,但如果你想迭代一個(gè)對(duì)象的屬性,你可以用for-in循環(huán)(這也是它的本職工作)或內(nèi)建的Object.keys()方法:

// 向控制臺(tái)輸出對(duì)象的可枚舉屬性
for (var key of Object.keys(someObject)) {
  console.log(key + ": " + someObject[key]);
}

深入理解

“能工摹形,巧匠竊意。”——巴勃羅·畢卡索

ES6始終堅(jiān)持這樣的宗旨:凡是新加入的特性,勢(shì)必已在其它語(yǔ)言中得到強(qiáng)有力的實(shí)用性證明。

舉個(gè)例子,新加入的for-of循環(huán)像極了C++、Java、C#以及Python中的循環(huán)語(yǔ)句。與它們一樣,這里的for-of循環(huán)支持語(yǔ)言和標(biāo)準(zhǔn)庫(kù)中提供的幾種不同的數(shù)據(jù)結(jié)構(gòu)。它同樣也是這門語(yǔ)言中的一個(gè)擴(kuò)展點(diǎn)(譯注:關(guān)于擴(kuò)展點(diǎn),建議參考 1. 淺析擴(kuò)展點(diǎn) 2. What are extensions and extension points?)。

正如其它語(yǔ)言中的for/foreach語(yǔ)句一樣,for-of****循環(huán)語(yǔ)句通過方法調(diào)用來遍歷各種集合。數(shù)組、Maps對(duì)象、Sets對(duì)象以及其它在我們討論的對(duì)象有一個(gè)共同點(diǎn),它們都有一個(gè)迭代器方法。

你可以給任意類型的對(duì)象添加迭代器方法。

當(dāng)你為對(duì)象添加myObject.toString()方法后,就可以將對(duì)象轉(zhuǎn)化為字符串,同樣地,當(dāng)你向任意對(duì)象添加myObjectSymbol.iterator方法,就可以遍歷這個(gè)對(duì)象了。

舉個(gè)例子,假設(shè)你正在使用jQuery,盡管你非常鐘情于里面的.each()方法,但你還是想讓jQuery對(duì)象也支持for-of循環(huán),你可以這樣做:

// 因?yàn)閖Query對(duì)象與數(shù)組相似
// 可以為其添加與數(shù)組一致的迭代器方法
jQuery.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator];

好的,我知道你在想什么,那個(gè)[Symbol.iterator]語(yǔ)法看起來很奇怪,這段代碼到底做了什么呢?這里通過Symbol處理了一下方法的名稱。標(biāo)準(zhǔn)委員會(huì)可以把這個(gè)方法命名為.iterator()方法,但是如果你的代碼中的對(duì)象可能也有一些.iterator()方法,這一定會(huì)讓你感到非常困惑。于是在ES6標(biāo)準(zhǔn)中使用symbol來作為方法名,而不是使用字符串。

你大概也猜到了,Symbols是ES6中的新類型,我們會(huì)在后續(xù)的文章中講解。現(xiàn)在,你需要記住,基于新標(biāo)準(zhǔn),你可以定義一個(gè)全新的symbol,就像Symbol.iterator,如此一來可以保證不與任何已有代碼產(chǎn)生沖突。這樣做的代價(jià)是,這段代碼的語(yǔ)法看起來會(huì)略顯生硬,但是這微乎其微代價(jià)卻可以為你帶來如此多的新特性和新功能,并且你所做的這一切可以完美地向后兼容。

所有擁有Symbol.iterator的對(duì)象被稱為可迭代的。在接下來的文章中你會(huì)發(fā)現(xiàn),可迭代對(duì)象的概念幾乎貫穿于整門語(yǔ)言之中,不僅是for-of循環(huán),還有Map和Set構(gòu)造函數(shù)、解構(gòu)賦值,以及新的展開操作符。

迭代器對(duì)象

現(xiàn)在,你將無須親自從零開始實(shí)現(xiàn)一個(gè)對(duì)象迭代器,我們會(huì)在下一篇文章詳細(xì)講解。為了幫助你理解本文,我們簡(jiǎn)單了解一下迭代器(如果你跳過這一章,你將錯(cuò)過非常精彩的技術(shù)細(xì)節(jié))。

for-of循環(huán)首先調(diào)用集合的Symbol.iterator方法,緊接著返回一個(gè)新的迭代器對(duì)象。迭代器對(duì)象可以是任意具有.next()方法的對(duì)象;for-of循環(huán)將重復(fù)調(diào)用這個(gè)方法,每次循環(huán)調(diào)用一次。舉個(gè)例子,這段代碼是我能想出來的最簡(jiǎn)單的迭代器:

var zeroesForeverIterator = {
 [Symbol.iterator]: function () {
   return this;
  },
  next: function () {
  return {done: false, value: 0};
 }
};

每一次調(diào)用.next()方法,它都返回相同的結(jié)果,返回給for-of循環(huán)的結(jié)果有兩種可能:(a) 我們尚未完成迭代;(b) 下一個(gè)值為0。這意味著(value of zeroesForeverIterator) {}將會(huì)是一個(gè)無限循環(huán)。當(dāng)然,一般來說迭代器不會(huì)如此簡(jiǎn)單。

這個(gè)迭代器的設(shè)計(jì),以及它的.done和.value屬性,從表面上看與其它語(yǔ)言中的迭代器不太一樣。在Java中,迭代器有分離的.hasNext()和.next()方法。在Python中,他們只有一個(gè).next() 方法,當(dāng)沒有更多值時(shí)拋出StopIteration異常。但是所有這三種設(shè)計(jì)從根本上講都返回了相同的信息。

迭代器對(duì)象也可以實(shí)現(xiàn)可選的.return()和.throw(exc)方法。如果for-of循環(huán)過早退出會(huì)調(diào)用.return()方法,異常、break語(yǔ)句或return語(yǔ)句均可觸發(fā)過早退出。如果迭代器需要執(zhí)行一些清潔或釋放資源的操作,可以在.return()方法中實(shí)現(xiàn)。大多數(shù)迭代器方法無須實(shí)現(xiàn)這一方法。.throw(exc)方法的使用場(chǎng)景就更特殊了:for-of循環(huán)永遠(yuǎn)不會(huì)調(diào)用它。但是我們還是會(huì)在下一篇文章更詳細(xì)地講解它的作用。

現(xiàn)在我們已了解所有細(xì)節(jié),可以寫一個(gè)簡(jiǎn)單的for-of循環(huán)然后按照下面的方法調(diào)用重寫被迭代的對(duì)象。

首先是for-of循環(huán):

for (VAR of ITERABLE) {
  一些語(yǔ)句
}

然后是一個(gè)使用以下方法和少許臨時(shí)變量實(shí)現(xiàn)的與之前大致相當(dāng)?shù)氖纠?/p>

var $iterator = ITERABLE[Symbol.iterator]();
var $result = $iterator.next();
while (!$result.done) {
   VAR = $result.value;
   一些語(yǔ)句
   $result = $iterator.next();
 }

這段代碼沒有展示.return()方法是如何處理的,我們可以添加這部分代碼,但我認(rèn)為這對(duì)于我們正在講解的內(nèi)容來說過于復(fù)雜了。for-of循環(huán)用起來很簡(jiǎn)單,但是其背后有著非常復(fù)雜的機(jī)制。

我何時(shí)可以開始使用這一新特性?

目前,對(duì)于for-of循環(huán)新特性,所有最新版本Firefox都(部分)支持(譯注:從FF 13開始陸續(xù)支持相關(guān)功能,F(xiàn)F 36 - FF 40基本支持大部分特性),在Chrome中可以通過訪問 chrome://flags 并啟用“實(shí)驗(yàn)性JavaScript”來支持。微軟的Spartan瀏覽器支持,但是IE不支持。如果你想在web環(huán)境中使用這種新語(yǔ)法,同時(shí)需要支持IE和Safari,你可以使用Babel或Google的Traceur這些編譯器來將你的ES6代碼翻譯為Web友好的ES5代碼。

而在服務(wù)端,你不需要類似的編譯器,io.js中默認(rèn)支持ES6新語(yǔ)法(部分),在Node中需要添加--harmony選項(xiàng)來啟用相關(guān)特性。

{done: true}

喲!

好的,我們今天的講解就到這里,但是對(duì)于for-of循環(huán)的使用遠(yuǎn)沒有結(jié)束。

在ES6中有一種新的對(duì)象與for-of循環(huán)配合使用非常契合,我沒有提及它因?yàn)樗俏覀兿轮芪恼碌闹黝},我認(rèn)為這種新特性是ES6種最夢(mèng)幻的地方,如果你尚未在類似Python和C#的語(yǔ)言中遇到它,你一開始很可能會(huì)發(fā)現(xiàn)它令人難以置信,但是這是編寫迭代器最簡(jiǎn)單的方式,在重構(gòu)中非常有用,并且它很可能改變我們書寫異步代碼的方式,無論是在瀏覽器環(huán)境還是服務(wù)器環(huán)境,所以,下周的深入淺出 ES6 中,請(qǐng)務(wù)必一起來仔細(xì)看看 ES6 的生成器:generators。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容