本文為原創,作者為Mozilla Web、Jason Orendorff,譯者為Lenville。未經許可,拒絕任何形式的轉載。
ECMAScript 2016的發布只帶來了Array.prototype.includes和冪運算符兩個新特性(http://www.ecma-international.org/ecma-262/7.0/index.html)。而ECMAScript 6的發布,卻給Javascript帶了了重大變革。ES6中包含的許多新的語言特性,使JS變得更加強大,更富表現力。
- ECMAScript以及ES6的地位
編程語言JavaScript是ECMAScript的實現和擴展,由ECMA(一個類似W3C的標準組織)參與進行標準化。ECMAScript定義了:
語言語法 – 語法解析規則、關鍵字、語句、聲明、運算符等。
類型 – 布爾型、數字、字符串、對象等。
原型和繼承
內建對象和函數的標準庫 – JSON、Math、數組方法、對象自省方法等。
ECMAScript標準不定義HTML或CSS的相關功能,也不定義類似DOM(文檔對象模型)的Web API,這些都在獨立的標準中進行定義。ECMAScript涵蓋了各種環境中JS的使用場景,無論是瀏覽器環境還是類似node.js的非瀏覽器環境。
ES6:重大的版本升級
早在2009年,ES5剛剛發布,自那時起,ES標準委員會一直在緊鑼密鼓地籌備新的JS語言標準——ES6。
ES6是一次重大的版本升級,與此同時,由于ES6秉承著最大化兼容已有代碼的設計理念,你過去編寫的JS代碼將繼續正常運行。事實上,許多瀏覽器已經支持部分ES6特性,并將繼續努力實現其余特性。這意味著,在一些已經實現部分特性的瀏覽器中,你的JS代碼已經可以正常運行。如果到目前為止你尚未遇到任何兼容性問題,那么你很有可能將不會遇到這些問題,瀏覽器正飛速實現各種新特性。
版本號6
ECMAScript標準的歷史版本分別是1、2、3、5。
那么為什么沒有第4版?其實,在過去確實曾計劃發布提出巨量新特性的第4版,但最終卻因想法太過激進而慘遭廢除(這一版標準中曾經有一個極其復雜的支持泛型和類型推斷的內建靜態類型系統)。
ES4飽受爭議,當標準委員會最終停止開發ES4時,其成員同意發布一個相對謙和的ES5版本,隨后繼續制定一些更具實質性的新特性。這一明確的協商協議最終命名為“Harmony”,因此,ES5規范中包含這樣兩句話:
ECMAScript是一門充滿活力的語言,并在不斷進化中。
未來版本的規范中將持續進行重要的技術改進。
這一聲明許下了一個未來的承諾。
兌現承諾
2009年發布的改進版本ES5,引入了Object.create()(https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/create)、Object.defineProperty()、getters和setters、嚴格模式以及JSON對象。我已經使用過所有這些新特性,并且我非常喜歡ES5做出的改進。但事實上,這些改進并沒有深入影響我編寫JS代碼的方式,對我來說最大的革新大概就是新的數組方法:.map()、. filter()這些。
但是,ES6并非如此!經過持續幾年的磨礪,它已成為JS有史以來最實質的升級,新的語言和庫特性就像無主之寶,等待有識之士的發掘。新的語言特性涵蓋范圍甚廣,小到受歡迎的語法糖,例如箭頭函數(arrow functions)和簡單的字符串插值(string interpolation),大到燒腦的新概念,例如代理(proxies)和生成器(generators)。
- ES6徹底改變了編寫JS代碼的方式
ES6徹底改變了我們編寫JS代碼的方式,讓我們就從經典的循環開始說起。
之前我們是怎么寫循環的
我們將從一個經典的“遺漏特性”說起,十年來我一直期待在JavaScript中看到的它。所以從現在起就加入我們吧,一起領略一下ES6迭代器(iterators)和新的for-of循環!
我們如何遍歷數組中的元素?20年前JavaScript剛萌生時,你可能這樣實現數組遍歷:
for (var index = 0; index < myArray.length; index++) {
console.log(myArray[index]);
}
自ES5正式發布后,你可以使用內建的forEach(https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/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循環體除了遍歷數組元素外,還會遍歷自定義屬性(https://developer.mozilla.org/en-US/docs/Glossary/Expando)。舉個例子,如果你的數組中有一個可枚舉屬性myArray.name,循環將額外執行一次,遍歷到名為“name”的索引。就連數組原型鏈上的屬性都能被訪問到。
最讓人震驚的是,在某些情況下,這段代碼可能按照隨機順序遍歷數組元素。
簡而言之,for-in是為普通對象設計的,你可以遍歷得到字符串類型的鍵,因此不適用于數組遍歷。
強大的for-of循環
還記得前面我向你們承諾過的話么?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對象(https://developer.mozilla.org/en-US/docs/Web/API/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. 淺析擴展點http://www.blogjava.net/yangbutao/archive/2007/09/27/148500.html 2. What are extensions and extension points?https://wiki.eclipse.org/FAQWhatareextensionsandextensionpoints%3F)。
正如其它語言中的for/foreach語句一樣,for-of循環語句通過方法調用來遍歷各種集合。數組、Maps對象、Sets對象以及其它在我們討論的對象有一個共同點,它們都有一個迭代器方法。
你可以給任意類型的對象添加迭代器方法。
當你為對象添加myObject.toString()方法后,就可以將對象轉化為字符串,同樣地,當你向任意對象添加myObjectSymbol.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 = ITERABLESymbol.iterator;
var $result = $iterator.next();
while (!$result.done) {
VAR = $result.value;
一些語句
$result = $iterator.next();
}
這段代碼沒有展示.return()方法是如何處理的,我們可以添加這部分代碼,但我認為這對于我們正在講解的內容來說過于復雜了。for-of循環用起來很簡單,但是其背后有著非常復雜的機制。
- 后記
本文的講解就到這里,但是對于for-of循環的使用遠沒有結束。
在ES6中有一種新的對象與for-of循環配合使用非常契合,我沒有提及它因為它是我們下篇文章的主題,我認為這種新特性是ES6種最夢幻的地方,如果你尚未在類似Python和C#的語言中遇到它,你一開始很可能會發現它令人難以置信,但是這是編寫迭代器最簡單的方式,在重構中非常有用,并且它很可能改變我們書寫異步代碼的方式,無論是在瀏覽器環境還是服務器環境,所以,在下篇文章中,請務必和我一起來仔細看看 ES6 的生成器:generators。