本文內(nèi)容
- ECMAScript 發(fā)生了什么變化?
- 新標(biāo)準(zhǔn)
- 版本號(hào)6
- 兌現(xiàn)承諾
- 迭代器和for-of循環(huán)
- 生成器 Generators
- 模板字符串
- 不定參數(shù)和默認(rèn)參數(shù)
- 解構(gòu) Destructuring
- 箭頭函數(shù) Arrow Functions
- Symbols
- 集合
- 學(xué)習(xí)Babel和Broccoli,馬上就用ES6
- 代理 Proxies
ES6 說(shuō)自己的宗旨是“凡是新加入的特性,勢(shì)必已在其它語(yǔ)言中得到強(qiáng)有力的實(shí)用性證明。”——TRUE!如果你大概瀏覽下 ES6 的新特性,事實(shí)上它們都不是什么新東西,而是在其他語(yǔ)言中已經(jīng)被廣泛認(rèn)可和采用的,還有就是多年工程實(shí)踐的結(jié)果,比如,JAVAScript 框架 jQuery、Undercore、AnjularJS、Backbone、React、Ember、Polymer、Knockout 和 Browserify、RequireJS、Webpack,以及NPM 和 Bower,涉及到 JavaScript 的庫(kù)和框架、模塊打包器及測(cè)試、任務(wù)調(diào)度器、包和工作流管理等方面,以前需要用這些三方框架來(lái)實(shí)現(xiàn),有些現(xiàn)在則不用了。因?yàn)椋珽S6 本身就具備。所以,以后寫(xiě) JS 代碼,或多或少跟像 Java、C# 等這些服務(wù)器端語(yǔ)言有點(diǎn)像~
如果你嫌內(nèi)容太長(zhǎng),可以大概瀏覽一下也行~你會(huì)發(fā)現(xiàn)服務(wù)器編程語(yǔ)言很多特性,現(xiàn)在在前端也能使用了~
ECMAScript 發(fā)生了什么變化?
JavaScript是ECMAScript的實(shí)現(xiàn)和擴(kuò)展,由ECMA(一個(gè)類(lèi)似W3C的標(biāo)準(zhǔn)組織)參與進(jìn)行標(biāo)準(zhǔn)化。ECMAScript定義了:
- 語(yǔ)言語(yǔ)法 – 語(yǔ)法解析規(guī)則、關(guān)鍵字、語(yǔ)句、聲明、運(yùn)算符等。
- 類(lèi)型 – 布爾型、數(shù)字、字符串、對(duì)象等。
- 原型和繼承
- 內(nèi)建對(duì)象和函數(shù)的標(biāo)準(zhǔn)庫(kù) – JSON、Math、數(shù)組方法、對(duì)象自省方法等。
ECMAScript標(biāo)準(zhǔn)不定義html或css的相關(guān)功能,也不定義類(lèi)似DOM(文檔對(duì)象模型)的Web API,這些都在其他的標(biāo)準(zhǔn)中定義。
ECMAScript涵蓋了各種環(huán)境中JS的使用場(chǎng)景,無(wú)論是瀏覽器環(huán)境還是類(lèi)似node.js的非瀏覽器環(huán)境。
新標(biāo)準(zhǔn)
2015年6月,ECMAScript語(yǔ)言規(guī)范第6版最終草案提請(qǐng)Ecma大會(huì)審查,這意味著什么呢?——我們將迎來(lái)最新的JavaScript核心語(yǔ)言標(biāo)準(zhǔn)。
早在2009年,上一版的ES5,自那時(shí)起,ES標(biāo)準(zhǔn)委員會(huì)一直在緊鑼密鼓地籌備新的JS語(yǔ)言標(biāo)準(zhǔn)——ES6。
ES6是一次重大的版本升級(jí),與此同時(shí),由于ES6秉承著最大化兼容已有代碼的設(shè)計(jì)理念,你過(guò)去編寫(xiě)的JS代碼將繼續(xù)正常運(yùn)行。事實(shí)上,許多瀏覽器已經(jīng)支持部分ES6特性,并將繼續(xù)努力實(shí)現(xiàn)其余特性。這意味著,在一些已經(jīng)實(shí)現(xiàn)部分特性的瀏覽器中,你的JS代碼已經(jīng)可以正常運(yùn)行。如果到目前為止你尚未遇到任何兼容性問(wèn)題,那么你很有可能將不會(huì)遇到這些問(wèn)題,瀏覽器正飛速實(shí)現(xiàn)各種新特性。
版本號(hào)6
ECMAScript標(biāo)準(zhǔn)的歷史版本分別是1、2、3、5。
為什么沒(méi)有版4?其實(shí),的確曾經(jīng)計(jì)劃發(fā)布具有大量新特性的版4,但最終因想法太過(guò)激進(jìn)而慘遭廢除(這一版標(biāo)準(zhǔn)中曾經(jīng)有一個(gè)極其復(fù)雜的支持泛型和類(lèi)型推斷的內(nèi)建靜態(tài)類(lèi)型系統(tǒng))。步子不能邁得太大~
ES4飽受爭(zhēng)議,當(dāng)標(biāo)準(zhǔn)委員會(huì)最終停止開(kāi)發(fā)ES4時(shí),其成員同意發(fā)布一個(gè)相對(duì)謙和的ES5版本,隨后繼續(xù)制定一些更具實(shí)質(zhì)性的新特性。這一明確的協(xié)商協(xié)議最終命名為“Harmony”,因此,ES5規(guī)范中包含這樣兩句話:
ECMAScript是一門(mén)充滿活力的語(yǔ)言,并在不斷進(jìn)化中。
未來(lái)版本的規(guī)范中將持續(xù)進(jìn)行重要的技術(shù)改進(jìn)。
兌現(xiàn)承諾
2009年的版5,引入了Object.create()、Object.defineProperty()、getters 和 setters、嚴(yán)格模式以及JSON對(duì)象。我已經(jīng)使用過(guò)所有這些新特性,并且非常喜歡。但這些改進(jìn)并沒(méi)有影響我編寫(xiě)JS代碼的方式,對(duì)我來(lái)說(shuō),最大的革新就是新的數(shù)組方法:.map()、. filter()。
但ES6并非如此!經(jīng)過(guò)持續(xù)幾年的磨礪,它已成為JS有史以來(lái)最實(shí)質(zhì)的升級(jí),新的語(yǔ)言和庫(kù)特性就像無(wú)主之寶,等待有識(shí)之士的發(fā)掘。新特性涵蓋范圍甚廣,小到受歡迎的語(yǔ)法糖,例如箭頭函數(shù)(arrow functions)和簡(jiǎn)單的字符串插值(string interpolation),大到燒腦的新概念,例如代理(proxies)和生成器(generators)。
ES6將徹底改變你編寫(xiě)JS代碼的方式!
下面從一個(gè)經(jīng)典的“遺漏特性”說(shuō)起,十年來(lái)我一直期待在JavaScript中看到的它——ES6迭代器(iterators)和新的for-of循環(huán)!
迭代器和for-of循環(huán)
如何遍歷數(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 方法來(lái)遍歷數(shù)組:
myArray.forEach(function (value) {
console.log(value);
});
這段代碼看起來(lái)更簡(jiǎn)潔,但有一個(gè)小缺陷:不能使用 break 語(yǔ)句中斷循環(huán),也不能使用 return 語(yǔ)句返回到外層函數(shù)。
當(dāng)然,如果只用 for 循環(huán)的語(yǔ)法來(lái)遍歷數(shù)組元素,那么,你一定想嘗試一下 for-in 循環(huán):
for (var index in myArray) {
console.log(myArray[index]);
}
但這絕對(duì)是一個(gè)糟糕的選擇,為什么呢?
- 這段代碼中,賦給 index 的值不是實(shí)際的數(shù)字,而是字符串“0”、“1”、“2”,此時(shí)很可能在無(wú)意間進(jìn)行字符串算數(shù)計(jì)算,例如:“2” + 1 == “21”,這帶來(lái)極大的不便。
- 作用于數(shù)組的for-in循環(huán)體除了遍歷數(shù)組元素外,還會(huì)遍歷自定義屬性。舉個(gè)例子,如果你的數(shù)組中有一個(gè)可枚舉屬性 myArray.name,循環(huán)將額外執(zhí)行一次,遍歷到名為“name”的索引。就連數(shù)組原型鏈上的屬性都能被訪問(wèn)到。
- 最讓人震驚的是,在某些情況下,這段代碼可能按照隨機(jī)順序遍歷數(shù)組元素。
- 簡(jiǎn)而言之,for-in是為普通對(duì)象設(shè)計(jì)的,你可以遍歷得到字符串類(lèi)型的鍵,因此不適用于數(shù)組遍歷。
強(qiáng)大的for-of循環(huán)
目前來(lái)看,成千上萬(wàn)的Web網(wǎng)站依賴 for-in 循環(huán),其中一些網(wǎng)站甚至將其用于數(shù)組遍歷。如果想通過(guò)修正for-in循環(huán)增加數(shù)組遍歷支持會(huì)讓這一切變得更加混亂,因此,標(biāo)準(zhǔn)委員會(huì)在ES6中增加了一種新的循環(huán)語(yǔ)法來(lái)解決目前的問(wèn)題。像下面那樣:
for (var value of myArray) {
console.log(value);
}
是的,與之前的內(nèi)建方法相比,這種循環(huán)方式看起來(lái)是否有些眼熟?那好,我們將要探究一下 for-of 循環(huán)的外表下隱藏著哪些強(qiáng)大的功能。現(xiàn)在,只需記住:
- 這是最簡(jiǎn)潔、最直接的遍歷數(shù)組元素的語(yǔ)法;
- 與forEach()不同的是,它可以正確響應(yīng) break、continue 和 return 語(yǔ)句;
- 這個(gè)方法避開(kāi)了for-in循環(huán)的所有缺陷。
for-in循環(huán)用來(lái)遍歷對(duì)象屬性。for-of循環(huán)用來(lái)遍歷數(shù)據(jù)—例如數(shù)組中的值。
但是,不僅如此!
for-of循環(huán)也可以遍歷其它的集合。for-of循環(huán)不僅支持?jǐn)?shù)組,還支持大多數(shù)類(lèi)數(shù)組對(duì)象,例如DOM NodeList對(duì)象。for-of循環(huán)也支持字符串遍歷,它將字符串視為一系列的Unicode字符來(lái)進(jìn)行遍歷:
for (var chr of "") {
alert(chr);
}
它同樣支持遍歷 Map 和 Set 對(duì)象。
對(duì)不起,你一定沒(méi)聽(tīng)說(shuō)過(guò)Map和Set對(duì)象。他們是ES6中新增的類(lèi)型。我們將在后面講解這兩個(gè)新的類(lèi)型。如果你曾在其它語(yǔ)言中使用過(guò)Map和Set,你會(huì)發(fā)現(xiàn)ES6中并無(wú)太大出入。
舉個(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ì)象稍有不同。數(shù)據(jù)由鍵值對(duì)組成,所以你需要使用解構(gòu)(destructuring)來(lái)將鍵值對(duì)拆解為兩個(gè)獨(dú)立的變量:
for (var [key, value] of phoneBookMap) {
console.log(key + "'s phone number is: " + value);
}
解構(gòu)也是ES6的新特性,我們將在后面講解。
現(xiàn)在,你只需記住:未來(lái)的JS可以使用一些新型的集合類(lèi),甚至?xí)懈嗟念?lèi)型陸續(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í)用性證明。
for-of 循環(huán)這個(gè)新特性,像極了 C++、Java、C# 以及 Python 中的 foreach 循環(huán)語(yǔ)句。與它們一樣,for-of循環(huán)支持語(yǔ)言和標(biāo)準(zhǔn)庫(kù)中提供的幾種不同的數(shù)據(jù)結(jié)構(gòu)。它同樣也是這門(mén)語(yǔ)言中的一個(gè)擴(kuò)展點(diǎn)。
正如其它語(yǔ)言中的for/foreach語(yǔ)句一樣,for-of循環(huán)語(yǔ)句通過(guò)方法調(diào)用來(lái)遍歷各種集合。數(shù)組、Maps對(duì)象、Sets對(duì)象以及其它在我們討論的對(duì)象有一個(gè)共同點(diǎn),它們都有一個(gè)迭代器方法。
你可以給任意類(lèi)型的對(duì)象添加迭代器方法。
當(dāng)你為對(duì)象添加myObject.toString()方法后,就可以將對(duì)象轉(zhuǎn)化為字符串,同樣地,當(dāng)你向任意對(duì)象添加myObject[Symbol.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ǔ)法看起來(lái)很奇怪,這段代碼到底做了什么呢?這里通過(guò)Symbol處理了一下方法的名稱。標(biāo)準(zhǔn)委員會(huì)可以把這個(gè)方法命名為.iterator()方法,但是如果你的代碼中的對(duì)象可能也有一些.iterator()方法,這一定會(huì)讓你感到非常困惑。于是在ES6標(biāo)準(zhǔn)中使用symbol來(lái)作為方法名,而不是使用字符串。
你大概也猜到了,Symbols是ES6中的新類(lèi)型,我們會(huì)在后續(xù)的文章中講解。現(xiàn)在,你需要記住,基于新標(biāo)準(zhǔn),你可以定義一個(gè)全新的 symbol,就像Symbol.iterator,如此一來(lái)可以保證不與任何已有代碼產(chǎn)生沖突。這樣做的代價(jià)是,這段代碼的語(yǔ)法看起來(lái)會(huì)略顯生硬,但是這微乎其微代價(jià)卻可以為你帶來(lái)如此多的新特性和新功能,并且你所做的這一切可以完美地向后兼容。
所有擁有[Symbol.iterator]()的對(duì)象被稱為可迭代的。在接下來(lái)的文章中你會(huì)發(fā)現(xiàn),可迭代對(duì)象的概念幾乎貫穿于整門(mén)語(yǔ)言之中,不僅是for-of循環(huán),還有Map和Set構(gòu)造函數(shù)、解構(gòu)賦值,以及新的展開(kāi)操作符。
迭代器對(duì)象
現(xiàn)在,你將無(wú)須親自從零開(kāi)始實(shí)現(xiàn)一個(gè)對(duì)象迭代器,我們會(huì)在下一篇文章詳細(xì)講解。為了幫助你理解本文,我們簡(jiǎn)單了解一下迭代器(如果你跳過(guò)這一章,你將錯(cuò)過(guò)非常精彩的技術(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è)例子,這段代碼是我能想出來(lái)的最簡(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è)無(wú)限循環(huán)。當(dāng)然,一般來(lái)說(shuō)迭代器不會(huì)如此簡(jiǎn)單。
這個(gè)迭代器的設(shè)計(jì),以及它的.done和.value屬性,從表面上看與其它語(yǔ)言中的迭代器不太一樣。在Java中,迭代器有分離的.hasNext()和.next()方法。在Python中,他們只有一個(gè).next() 方法,當(dāng)沒(méi)有更多值時(shí)拋出StopIteration異常。但是所有這三種設(shè)計(jì)從根本上講都返回了相同的信息。
迭代器對(duì)象也可以實(shí)現(xiàn)可選的.return()和.throw(exc)方法。如果for-of循環(huán)過(guò)早退出會(huì)調(diào)用.return()方法,異常、 break語(yǔ)句或return語(yǔ)句均可觸發(fā)過(guò)早退出。如果迭代器需要執(zhí)行一些清潔或釋放資源的操作,可以在.return()方法中實(shí)現(xiàn)。大多數(shù)迭代器方法無(wú)須實(shí)現(xiàn)這一方法。.throw(exc)方法的使用場(chǎng)景就更特殊了:for-of循環(huán)永遠(yuǎn)不會(huì)調(diào)用它。但是我們還是會(huì)在下一篇文章更詳細(xì)地講解它的作用。
現(xiàn)在我們已了解所有細(xì)節(jié),可以寫(xiě)一個(gè)簡(jiǎn)單的for-of循環(huán)然后按照下面的方法調(diào)用重寫(xiě)被迭代的對(duì)象。
首先是for-of循環(huán):
for (VAR of ITERABLE) {
// do something
}
然后是一個(gè)使用以下方法和少許臨時(shí)變量實(shí)現(xiàn)的與之前大致相當(dāng)?shù)氖纠?/p>
var $iterator = ITERABLE[Symbol.iterator]();
var $result = $iterator.next();
while (!$result.done) {
VAR = $result.value;
// do something
$result = $iterator.next();
}
這段代碼沒(méi)有展示.return()方法是如何處理的,我們可以添加這部分代碼,但我認(rèn)為這對(duì)于我們正在講解的內(nèi)容來(lái)說(shuō)過(guò)于復(fù)雜了。for-of循環(huán)用起來(lái)很簡(jiǎn)單,但是其背后有著非常復(fù)雜的機(jī)制。
我何時(shí)可以開(kāi)始使用這一新特性?
目前,對(duì)于for-of循環(huán)新特性,所有最新版本Firefox都(部分)支持(譯注:從FF 13開(kāi)始陸續(xù)支持相關(guān)功能,F(xiàn)F 36 - FF 40基本支持大部分特性),在Chrome中可以通過(guò)訪問(wèn) chrome://flags 并啟用“實(shí)驗(yàn)性JavaScript”來(lái)支持。微軟的Spartan瀏覽器支持,但是IE不支持。如果你想在web環(huán)境中使用這種新語(yǔ)法,同時(shí)需要支持 IE和Safari,你可以使用Babel或google的Traceur這些編譯器來(lái)將你的ES6代碼翻譯為Web友好的ES5代碼。
而在服務(wù)端,你不需要類(lèi)似的編譯器,io.js中默認(rèn)支持ES6新語(yǔ)法(部分),在Node中需要添加--harmony選項(xiàng)來(lái)啟用相關(guān)特性。
{done: true}
for-of 循環(huán)的使用遠(yuǎn)沒(méi)有結(jié)束。
在ES6中有一種新的對(duì)象與for-of循環(huán)配合使用非常契合,后面將講解。我認(rèn)為這種新特性是ES6種最夢(mèng)幻的地方——ES6 的生成器:generators,如果你尚未在類(lèi)似Python和C#的語(yǔ)言中遇到它,你一開(kāi)始很可能會(huì)發(fā)現(xiàn)它令人難以置信,但是這是編寫(xiě)迭代器最簡(jiǎn)單的方式,在重構(gòu)中非常有用,并且它很可能改變我們書(shū)寫(xiě)異步代碼的方式,無(wú)論是在瀏覽器環(huán)境還是服務(wù)器環(huán)境 。
生成器 Generators
為什么說(shuō)是“最具魔力的”?對(duì)于初學(xué)者來(lái)說(shuō),此特性與JS之前已有的特性截然不同,可能會(huì)覺(jué)得有點(diǎn)晦澀難懂。但是,從某種意義上來(lái)說(shuō),它使語(yǔ)言內(nèi)部的常態(tài)行為變得更加強(qiáng)大,如果這都不算有魔力,我不知道還有什么能算。
不僅如此,此特性可以極大地簡(jiǎn)化代碼,它甚至可以幫助你逃離“回調(diào)地獄”。
既然新特性如此神奇,那么就一起深入了解它的魔力吧!
什么是生成器?
我們從一個(gè)示例開(kāi)始:
function* quips(name) {
yield "你好 " + name + "!";
yield "希望你能喜歡這篇介紹ES6的譯文";
if (name.startsWith("X")) {
yield "你的名字 " + name + " 首字母是X,這很酷!";
}
yield "我們下次再見(jiàn)!";
}
這是一只會(huì)說(shuō)話的貓,這段代碼很可能代表著當(dāng)今互聯(lián)網(wǎng)上最重要的一類(lèi)應(yīng)用。(試著點(diǎn)擊這個(gè)鏈接,與這只貓互動(dòng)一下,如果你感到有些困惑,回到這里繼續(xù)閱讀)。
這段代碼看起來(lái)很像一個(gè)函數(shù),我們稱之為生成器函數(shù),它與普通函數(shù)有很多共同點(diǎn),但是二者有如下區(qū)別:
- 普通函數(shù)使用function聲明,而生成器函數(shù)使用function*聲明。
- 在生成器函數(shù)內(nèi)部,有一種類(lèi)似return的語(yǔ)法:關(guān)鍵字yield。二者的區(qū)別是,普通函數(shù)只可以return一次,而生成器函數(shù)可以yield多次(當(dāng)然也可以只yield一次)。在生成器的執(zhí)行過(guò)程中,遇到y(tǒng)ield表達(dá)式立即暫停,后續(xù)可恢復(fù)執(zhí)行狀態(tài)。
這就是普通函數(shù)和生成器函數(shù)之間最大的區(qū)別,普通函數(shù)不能自暫停,生成器函數(shù)可以。
生成器做了什么?
當(dāng)你調(diào)用quips()生成器函數(shù)時(shí)發(fā)生了什么?
> var iter = quips("jorendorff");
[object Generator]
> iter.next()
{ value: "你好 jorendorff!", done: false }
> iter.next()
{ value: "希望你能喜歡這篇介紹ES6的譯文", done: false }
> iter.next()
{ value: "我們下次再見(jiàn)!", done: false }
> iter.next()
{ value: undefined, done: true }
你大概已經(jīng)習(xí)慣了普通函數(shù)的使用方式,當(dāng)你調(diào)用它們時(shí),它們立即開(kāi)始運(yùn)行,直到遇到return或拋出異常時(shí)才退出執(zhí)行,作為JS程序員你一定深諳此道。
生成器調(diào)用看起來(lái)非常類(lèi)似:quips("jorendorff")。但是,當(dāng)你調(diào)用一個(gè)生成器時(shí),它并非立即執(zhí)行,而是返回一個(gè)已暫停的生成器對(duì)象(上述實(shí)例代碼中的iter)。你可將這個(gè)生成器對(duì)象視為一次函數(shù)調(diào)用,只不過(guò)立即凍結(jié)了,它恰好在生成器函數(shù)的最頂端的第一行代碼之前凍結(jié)了。
每當(dāng)你調(diào)用生成器對(duì)象的.next()方法時(shí),函數(shù)調(diào)用將其自身解凍并一直運(yùn)行到下一個(gè)yield表達(dá)式,再次暫停。
這也是在上述代碼中我們每次都調(diào)用iter.next()的原因,我們獲得了quips()函數(shù)體中yield表達(dá)式生成的不同的字符串值。
調(diào)用最后一個(gè)iter.next()時(shí),我們最終抵達(dá)生成器函數(shù)的末尾,所以返回結(jié)果中done的值為true。抵達(dá)函數(shù)的末尾意味著沒(méi)有返回值,所以返回結(jié)果中value的值為undefined。
現(xiàn)在回到會(huì)說(shuō)話的貓的demo頁(yè)面,嘗試在循環(huán)中加入一個(gè)yield,會(huì)發(fā)生什么?
如果用專業(yè)術(shù)語(yǔ)描述,每當(dāng)生成器執(zhí)行yields語(yǔ)句,生成器的堆棧結(jié)構(gòu)(本地變量、參數(shù)、臨時(shí)值、生成器內(nèi)部當(dāng)前的執(zhí)行位置)被移出堆棧。然而,生成器對(duì)象保留了對(duì)這個(gè)堆棧結(jié)構(gòu)的引用(備份),所以稍后調(diào)用.next()可以重新激活堆棧結(jié)構(gòu)并且繼續(xù)執(zhí)行。
值得特別一提的是,生成器不是線程,在支持線程的語(yǔ)言中,多段代碼可以同時(shí)運(yùn)行,通通常導(dǎo)致競(jìng)態(tài)條件和非確定性,不過(guò)同時(shí)也帶來(lái)不錯(cuò)的性能。生成器則完全不同。當(dāng)生成器運(yùn)行時(shí),它和調(diào)用者處于同一線程中,擁有確定的連續(xù)執(zhí)行順序,永不并發(fā)。與系統(tǒng)線程不同的是,生成器只有在其函數(shù)體內(nèi)標(biāo)記為yield的點(diǎn)才會(huì)暫停。
現(xiàn)在,我們了解了生成器的原理,領(lǐng)略過(guò)生成器的運(yùn)行、暫停恢復(fù)運(yùn)行的不同狀態(tài)。那么,這些奇怪的功能究竟有何用處?
生成器是迭代器!
上周,我們學(xué)習(xí)了ES6的迭代器,它是ES6中獨(dú)立的內(nèi)建類(lèi),同時(shí)也是語(yǔ)言的一個(gè)擴(kuò)展點(diǎn),通過(guò)實(shí)現(xiàn)[Symbol.iterator]()和.next()兩個(gè)方法你就可以創(chuàng)建自定義迭代器。
實(shí)現(xiàn)一個(gè)接口不是一樁小事,我們一起實(shí)現(xiàn)一個(gè)迭代器。舉個(gè)例子,我們創(chuàng)建一個(gè)簡(jiǎn)單的range迭代器,它可以簡(jiǎn)單地將兩個(gè)數(shù)字之間的所有數(shù)相加。首先是傳統(tǒng)C的for(;;)循環(huán):
// 應(yīng)該彈出三次 "ding"
for (var value of range(0, 3)) {
alert("Ding! at floor #" + value);
}
使用ES6的類(lèi)的解決方案(如果不清楚語(yǔ)法細(xì)節(jié),無(wú)須擔(dān)心,我們將在接下來(lái)的文章中為你講解):
class RangeIterator {
constructor(start, stop) {
this.value = start;
this.stop = stop;
}
[Symbol.iterator]() { return this; }
next() {
var value = this.value;
if (value < this.stop) {
this.value++;
return {done: false, value: value};
} else {
return {done: true, value: undefined};
}
}
}
// 返回一個(gè)新的迭代器,可以從start到stop計(jì)數(shù)。
function range(start, stop) {
return new RangeIterator(start, stop);
}
查看代碼運(yùn)行情況。
這里的實(shí)現(xiàn)類(lèi)似Java或Swift中的迭代器,不是很糟糕,但也不是完全沒(méi)有問(wèn)題。我們很難說(shuō)清這段代碼中是否有bug,這段代碼看起來(lái)完全不像我們?cè)噲D模仿的傳統(tǒng)for (;;)循環(huán),迭代器協(xié)議迫使我們拆解掉循環(huán)部分。
此時(shí)此刻你對(duì)迭代器可能尚無(wú)感覺(jué),他們用起來(lái)很酷,但看起來(lái)有些難以實(shí)現(xiàn)。
你大概不會(huì)為了使迭代器更易于構(gòu)建從而建議我們?yōu)镴S語(yǔ)言引入一個(gè)離奇古怪又野蠻的新型控制流結(jié)構(gòu),但是既然我們有生成器,是否可以在這里應(yīng)用它們呢?一起嘗試一下:
function* range(start, stop) {
for (var i = start; i < stop; i++)
yield i;
}
查看代碼運(yùn)行情況。
以上4行代碼實(shí)現(xiàn)的生成器完全可以替代之前引入了一整個(gè)RangeIterator類(lèi)的23行代碼的實(shí)現(xiàn)。可行的原因是:生成器是迭代器。所有的生成器都有內(nèi)建.next()和[Symbol.iterator]()方法的實(shí)現(xiàn)。你只須編寫(xiě)循環(huán)部分的行為。
我們都非常討厭被迫用被動(dòng)語(yǔ)態(tài)寫(xiě)一封很長(zhǎng)的郵件,不借助生成器實(shí)現(xiàn)迭代器的過(guò)程與之類(lèi)似,令人痛苦不堪。當(dāng)你的語(yǔ)言不再簡(jiǎn)練,說(shuō)出的話就會(huì)變得難以理解。RangeIterator的實(shí)現(xiàn)代碼很長(zhǎng)并且非常奇怪,因?yàn)槟阈枰诓唤柚h(huán)語(yǔ)法的前提下為它添加循環(huán)功能的描述。所以生成器是最好的解決方案!
我們?nèi)绾伟l(fā)揮作為迭代器的生成器所產(chǎn)生的最大效力?
l 使任意對(duì)象可迭代。編寫(xiě)生成器函數(shù)遍歷這個(gè)對(duì)象,運(yùn)行時(shí)yield每一個(gè)值。然后將這個(gè)生成器函數(shù)作為這個(gè)對(duì)象的[Symbol.iterator]方法。
l 簡(jiǎn)化數(shù)組構(gòu)建函數(shù)。假設(shè)你有一個(gè)函數(shù),每次調(diào)用的時(shí)候返回一個(gè)數(shù)組結(jié)果,就像這樣:
// 拆分一維數(shù)組icons
// 根據(jù)長(zhǎng)度rowLength
function splitIntoRows(icons, rowLength) {
var rows = [];
for (var i = 0; i < icons.length; i += rowLength) {
rows.push(icons.slice(i, i + rowLength));
}
return rows;
}
使用生成器創(chuàng)建的代碼相對(duì)較短:
function* splitIntoRows(icons, rowLength) {
for (var i = 0; i < icons.length; i += rowLength) {
yield icons.slice(i, i + rowLength);
}
}
行為上唯一的不同是,傳統(tǒng)寫(xiě)法立即計(jì)算所有結(jié)果并返回一個(gè)數(shù)組類(lèi)型的結(jié)果,使用生成器則返回一個(gè)迭代器,每次根據(jù)需要逐一地計(jì)算結(jié)果。
- 獲取異常尺寸的結(jié)果。你無(wú)法構(gòu)建一個(gè)無(wú)限大的數(shù)組,但是你可以返回一個(gè)可以生成一個(gè)永無(wú)止境的序列的生成器,每次調(diào)用可以從中取任意數(shù)量的值。
- 重構(gòu)復(fù)雜循環(huán)。你是否寫(xiě)過(guò)又丑又大的函數(shù)?你是否愿意將其拆分為兩個(gè)更簡(jiǎn)單的部分?現(xiàn)在,你的重構(gòu)工具箱里有了新的利刃——生成器。當(dāng)你面對(duì)一個(gè)復(fù)雜的循環(huán)時(shí),你可以拆分出生成數(shù)據(jù)的代碼,將其轉(zhuǎn)換為獨(dú)立的生成器函數(shù),然后使用for (var data of myNewGenerator(args))遍歷我們所需的數(shù)據(jù)。
- 構(gòu)建與迭代相關(guān)的工具。ES6不提供用來(lái)過(guò)濾、映射以及針對(duì)任意可迭代數(shù)據(jù)集進(jìn)行特殊操作的擴(kuò)展庫(kù)。借助生成器,我們只須寫(xiě)幾行代碼就可以實(shí)現(xiàn)類(lèi)似的工具。
舉個(gè)例子,假設(shè)你需要一個(gè)等效于Array.prototype.filter并且支持DOM NodeLists的方法,可以這樣寫(xiě):
function* filter(test, iterable) {
for (var item of iterable) {
if (test(item))
yield item;
}
}
你看,生成器魔力四射!借助它們的力量可以非常輕松地實(shí)現(xiàn)自定義迭代器,記住,迭代器貫穿ES6的始終,它是數(shù)據(jù)和循環(huán)的新標(biāo)準(zhǔn)。
以上只是生成器的冰山一角,最重要的功能請(qǐng)繼續(xù)觀看!
生成器和異步代碼
這是我以前寫(xiě)的一些JS代碼:
};
})
});
});
});
});
可能你已經(jīng)見(jiàn)過(guò)類(lèi)似的代碼,異步API通常需要一個(gè)回調(diào)函數(shù),這意味著你需要為每一次任務(wù)執(zhí)行編寫(xiě)額外的異步函數(shù)。所以如果你有一段代碼需要完成三個(gè)任務(wù),你將看到類(lèi)似的三層級(jí)縮進(jìn)的代碼,而非簡(jiǎn)單的三行代碼。
后來(lái)我就這樣寫(xiě)了:
}).on('close', function () {
done(undefined, undefined);
}).on('error', function (error) {
done(error);
});
異步API擁有錯(cuò)誤處理規(guī)則,不支持異常處理。不同的API有不同的規(guī)則,大多數(shù)的錯(cuò)誤規(guī)則是默認(rèn)的;在有些API里,甚至連成功提示都是默認(rèn)的。
這些是到目前為止我們?yōu)楫惒骄幊趟冻龅拇鷥r(jià),我們正慢慢開(kāi)始接受異步代碼不如等效同步代碼美觀又簡(jiǎn)潔的這個(gè)事實(shí)。
生成器為你提供了避免以上問(wèn)題的新思路。
實(shí)驗(yàn)性的Q.async()嘗試結(jié)合promises使用生成器產(chǎn)生異步代碼的等效同步代碼。舉個(gè)例子:
// 制造一些噪音的同步代碼。
function makeNoise() {
shake();
rattle();
roll();
}
// 制造一些噪音的異步代碼。
// 返回一個(gè)Promise對(duì)象
// 當(dāng)我們制造完噪音的時(shí)候會(huì)變?yōu)閞esolved
function makeNoise_async() {
return Q.async(function* () {
yield shake_async();
yield rattle_async();
yield roll_async();
});
}
二者主要的區(qū)別是,異步版必須在每次調(diào)用異步函數(shù)的地方添加yield關(guān)鍵字。
在Q.async版本中添加一個(gè)類(lèi)似if語(yǔ)句的判斷或try/catch塊,如同向同步版本中添加類(lèi)似功能一樣簡(jiǎn)單。與其它異步代碼編寫(xiě)方法相比,這種方法更自然,不像學(xué)一門(mén)新語(yǔ)言一樣辛苦。
如果你已經(jīng)看到這里,你可以試著閱讀來(lái)自James Long的更深入地講解生成器的文章。
生成器為我們提供了一個(gè)新的異步編程模型思路,這種方法更適合人類(lèi)的大腦。相關(guān)工作正在不斷展開(kāi)。此外,更好的語(yǔ)法或許會(huì)有幫助,ES7中有一個(gè)有關(guān)異步函數(shù)的提案,它基于promises和生成器構(gòu)建,并從C#相似的特性中汲取了大量靈感。
如何應(yīng)用這些瘋狂的新特性?
在服務(wù)器端,現(xiàn)在你可以在io.js中使用ES6(在Node中你需要使用 –harmony 這個(gè)命令行選項(xiàng))。
在瀏覽器端,到目前為止只有Firefox 27+和Chrome 39+支持了ES6生成器。如果要在web端使用生成器,你需要使用Babel或Traceur來(lái)將你的ES6代碼轉(zhuǎn)譯為Web友好的ES5。
起初,JS中的生成器由Brendan Eich實(shí)現(xiàn),他的設(shè)計(jì)參考了Python生成器,而此Python生成器則受到Icon的啟發(fā)。他們?cè)缭?006年就在Firefox 2.0中移植了相關(guān)代碼。但是,標(biāo)準(zhǔn)化的道路崎嶇不平,相關(guān)語(yǔ)法和行為都在原先的基礎(chǔ)上有所改動(dòng)。Firefox和Chrome中的ES6生成器都是由編譯器hacker Andy Wingo實(shí)現(xiàn)的。這項(xiàng)工作由Bloomberg贊助支持(沒(méi)聽(tīng)錯(cuò),就是大名鼎鼎的那個(gè)彭博!)。
生成器還有更多未提及的特性,例如:.throw()和.return()方法、可選參數(shù).next()、yield*表達(dá)式語(yǔ)法。由于行文過(guò)長(zhǎng),估計(jì)觀眾已然疲乏,我們應(yīng)該學(xué)習(xí)一下生成器,暫時(shí)yield在這里,剩下的干貨擇機(jī)為大家獻(xiàn)上。
下一次,我們變換一下風(fēng)格,由于我們接連搬了兩座大山:迭代器和生成器,下次就一起研究下不會(huì)改變你編程風(fēng)格的ES6特性好不?就是一些簡(jiǎn)單又實(shí)用的東西,你一定會(huì)喜笑顏開(kāi)噠!你還別說(shuō),在什么都要“微”一下的今天,ES6當(dāng)然要有微改進(jìn)了!
續(xù)篇
回顧
在第三篇文章中,我們著重講解了生成器的基本行為。你可能對(duì)此感到陌生,但是并不難理解。生成器函數(shù)與普通函數(shù)有很多相似之處,它們之間最大的不同是,普通函數(shù)一次執(zhí)行完畢,而生成器函數(shù)體每次執(zhí)行一部分,每當(dāng)執(zhí)行到一個(gè)yield表達(dá)式的時(shí)候就會(huì)暫停。
盡管在那篇文章中我們進(jìn)行過(guò)詳細(xì)解釋,但我們始終未把所有特性結(jié)合起來(lái)給大家講解示例。現(xiàn)在就讓我們出發(fā)吧!
function* somewords() {
yield "hello";
yield "world";
}
for (var word of somewords()) {
alert(word);
}
這段腳本簡(jiǎn)單易懂,但是如果你把代碼中不同的比特位當(dāng)做戲劇中的任務(wù),你會(huì)發(fā)現(xiàn)它變得如此與眾不同。穿上新衣的代碼看起來(lái)是這樣的:
(譯者注:下面這是原作者創(chuàng)作的一個(gè)劇本,他將ES6中的各種函數(shù)和語(yǔ)法擬人化,以講解生成器(Generator)的實(shí)現(xiàn)原理)
場(chǎng)景 - 另一個(gè)世界的計(jì)算機(jī),白天
for loop女士獨(dú)自站在舞臺(tái)上,戴著一頂安全帽,手里拿著一個(gè)筆記板,上面記載著所有的事情。
for loop:
(電話響起)
somewords()!
generator出現(xiàn):這是一位高大的、有著一絲不茍紳士外表的黃銅機(jī)器人。
它看起來(lái)足夠友善,但給人的感覺(jué)仍然是冷冰冰的金屬。
for loop:
(瀟灑地拍了拍她的手)
好吧!我們?nèi)フ倚┦聝鹤霭伞? (對(duì)generator說(shuō))
.next()!
generator動(dòng)了起來(lái),就像突然擁有了生命。
generator:
{value: "hello", done: false}
然而猝不及防的,它以一個(gè)滑稽的姿勢(shì)停止了動(dòng)作。
for loop:
alert!
alert小子飛快沖進(jìn)舞臺(tái),眼睛大睜,上氣不接下氣。我們感覺(jué)的到他一向如此。
for loop:
對(duì)user說(shuō)“hello”。
alert小子轉(zhuǎn)身沖下舞臺(tái)。
alert:
(舞臺(tái)下,大聲尖叫)
一切都靜止了!
你正在訪問(wèn)的頁(yè)面說(shuō),
“hello”!
停留了幾秒鐘后,alert小子跑回舞臺(tái),穿過(guò)所有人滑停在for loop女士身邊。
alert:
user說(shuō)ok。
for loop:
(瀟灑地拍了拍她的手)
好吧!我們?nèi)フ倚┦聝鹤霭伞? (回到generator身邊)
.next()!
generator又一次煥發(fā)生機(jī)。
generator:
{value: "world", done: false}
它換了個(gè)姿勢(shì)又一次凍結(jié)。
for loop:
alert!
alert:
(已經(jīng)跑起來(lái))
正在搞定!
(舞臺(tái)下,大聲尖叫)
一切都靜止了!
你正在訪問(wèn)的頁(yè)面說(shuō),
“world”!
又一次暫停,然后alert突然跋涉回到舞臺(tái),垂頭喪氣的。
alert:
user再一次說(shuō)ok,但是…
但是請(qǐng)阻止這個(gè)頁(yè)面
創(chuàng)建額外的對(duì)話。
他噘著嘴離開(kāi)了。
for loop:
(瀟灑地拍了拍她的手)
好吧!我們?nèi)フ倚┦聝鹤霭伞? (回到generator身邊)
.next()!
generator第三次煥發(fā)生機(jī)。
generator:
(莊嚴(yán)的)
{value: undefined, done: true}
它的頭低下了,光芒從它的眼里消失。它不再移動(dòng)。
for loop
我的午餐時(shí)間到了。
她離開(kāi)了。
一會(huì)兒,garbage collector(垃圾收集器)老頭進(jìn)入,撿起了奄奄一息的generator,將它帶下舞臺(tái)。
好吧,這一出戲不太像哈姆雷特,但你應(yīng)該可以想象得出來(lái)。
正如你在戲劇中看到的,當(dāng)生成器對(duì)象第一次出現(xiàn)時(shí),它立即暫停了。每當(dāng)調(diào)用它的.next()方法,它都會(huì)蘇醒并向前執(zhí)行一部分。
所有動(dòng)作都是單線程同步的。請(qǐng)注意,無(wú)論何時(shí)永遠(yuǎn)只有一個(gè)真正活動(dòng)的角色,角色們不會(huì)互相打斷,亦不會(huì)互相討論,他們輪流講話,只要他們的話沒(méi)有說(shuō)完都可以繼續(xù)說(shuō)下去。(就像莎士比亞一樣!)
每當(dāng)for-of循環(huán)遍歷生成器時(shí),這出戲的某個(gè)版本就展開(kāi)了。這些.next()方法調(diào)用序列永遠(yuǎn)不會(huì)在你的代碼的任何角落出現(xiàn),在劇本里我把它們都放在舞臺(tái)上了,但是對(duì)于你和你的程序而言,所有這一切都應(yīng)該在幕后完成,因?yàn)樯善骱蚮or-of循環(huán)就是被設(shè)計(jì)成通過(guò)迭代器接口聯(lián)結(jié)工作的。
所以,總結(jié)一下到目前為止所有的一切:
- 生成器對(duì)象是可以產(chǎn)生值的優(yōu)雅的黃銅機(jī)器人。
- 每個(gè)生成器函數(shù)體構(gòu)成的單一代碼塊就是一個(gè)機(jī)器人。
如何關(guān)停生成器
我在第1部分沒(méi)有提到這些繁瑣的生成器特性:
- generator.return()
- generator.next()的可選參數(shù)
- generator.throw(error)
- yield*
如果你不理解這些特性存在得意義,就很難對(duì)它們提起興趣,更不用說(shuō)理解它們的實(shí)現(xiàn)細(xì)節(jié),所以我選擇直接跳過(guò)。但是當(dāng)我們深入學(xué)習(xí)生成器時(shí),勢(shì)必要仔細(xì)了解這些特性的方方面面。
你或許曾使用過(guò)這樣的模式:
function dothings() {
setup();
try {
// ... 做一些事情
} finally {
cleanup();
}
}
dothings();
清理(cleanup)過(guò)程包括關(guān)閉連接或文件,釋放系統(tǒng)資源,或者只是更新dom來(lái)關(guān)閉“運(yùn)行中”的加載動(dòng)畫(huà)。我們希望無(wú)論任務(wù)成功完成與否都觸發(fā)清理操作,所以執(zhí)行流入到finally代碼塊。
那么生成器中的清理操作看起來(lái)是什么樣的呢?
function* producevalues() {
setup();
try {
// ... 生成一些值
} finally {
cleanup();
}
}
for (var value of producevalues()) {
work(value);
}
這段代碼看起來(lái)很好,但是這里有一個(gè)問(wèn)題:我們沒(méi)在try代碼塊中調(diào)用work(value),如果它拋出異常,我們的清理步驟會(huì)如何執(zhí)行呢?
或者假設(shè)for-of循環(huán)包含一條break語(yǔ)句或return語(yǔ)句。清理步驟又會(huì)如何執(zhí)行呢?
放心,清理步驟無(wú)論如何都會(huì)執(zhí)行,ES6已經(jīng)為你做好了一切。
我們第一次討論迭代器和for-of循環(huán)時(shí)曾說(shuō)過(guò),迭代器接口支持一個(gè)可選的.return()方法,每當(dāng)?shù)诘鞣祷貃done:true}之前退出都會(huì)自動(dòng)調(diào)用這個(gè)方法。生成器支持這個(gè)方法,mygenerator.return()會(huì)觸發(fā)生成器執(zhí)行任一finally代碼塊然后退出,就好像當(dāng)前的生成暫停點(diǎn)已經(jīng)被秘密轉(zhuǎn)換為一條return語(yǔ)句一樣。
注意,.return()方法并不是在所有的上下文中都會(huì)被自動(dòng)調(diào)用,只有當(dāng)使用了迭代協(xié)議的情況下才會(huì)觸發(fā)該機(jī)制。所以也有可能生成器沒(méi)執(zhí)行finally代碼塊就直接被垃圾回收了。
如何在舞臺(tái)上模擬這些特性?生成器被凍結(jié)在一個(gè)需要一些配置的任務(wù)(例如,建造一幢摩天大樓)中間。突然有人拋出一個(gè)錯(cuò)誤!for循環(huán)捕捉到這個(gè)錯(cuò)誤并將它放置在一遍,她告訴生成器執(zhí)行.return()方法。生成器冷靜地拆除了所有腳手架并停工。然后for循環(huán)取回錯(cuò)誤,繼續(xù)執(zhí)行正常的異常處理過(guò)程。
生成器主導(dǎo)模式
到目前為止,我們?cè)趧”局锌吹降纳善鳎╣enerator)和使用者(user)之間的對(duì)話非常有限,現(xiàn)在換一種方式繼續(xù)解釋:
在這里使用者主導(dǎo)一切流程,生成器根據(jù)需要完成它的任務(wù),但這不是使用生成器進(jìn)行編程的唯一方式。
在第1部分中我曾經(jīng)說(shuō)過(guò),生成器可以用來(lái)實(shí)現(xiàn)異步編程,完成你用異步回調(diào)或promise鏈所做的一切。我知道你一定想知道它是如何實(shí)現(xiàn)的,為什么yield的能力(這可是生成器專屬的特殊能力)足夠應(yīng)對(duì)這些任務(wù)。畢竟,異步代碼不僅產(chǎn)生(yield)數(shù)據(jù),還會(huì)觸發(fā)事件,比如從文件或數(shù)據(jù)庫(kù)中調(diào)用數(shù)據(jù),向服務(wù)器發(fā)起請(qǐng)求并返回事件循環(huán)來(lái)等待異步過(guò)程結(jié)束。生成器如何實(shí)現(xiàn)這一切?它又是如何不借助回調(diào)力量從文件、數(shù)據(jù)庫(kù)或服務(wù)器中接受數(shù)據(jù)?
為了開(kāi)始找出答案,考慮一下如果.next()的調(diào)用者只有一種方法可以傳值返回給生成器會(huì)發(fā)生什么??jī)H僅是這一點(diǎn)改變,我們就可能創(chuàng)造一種全新的會(huì)話形式:
事實(shí)上,生成器的.next()方法接受一個(gè)可選參數(shù),參數(shù)稍后會(huì)作為yield表達(dá)式的返回值出現(xiàn)在生成器中。那就是說(shuō),yield語(yǔ)句與return語(yǔ)句不同,它是一個(gè)只有當(dāng)生成器恢復(fù)時(shí)才會(huì)有值的表達(dá)式。
var results = yield getdataandlatte(request.areacode);
這一行代碼完成了許多功能:
- 調(diào)用getdataandlatte(),假設(shè)函數(shù)返回我們?cè)诮貓D中看到的字符串“get me the database records for area code...”。
- 暫停生成器,生成字符串值。
- 此時(shí)可以暫停任意長(zhǎng)的時(shí)間。
- 最終,直到有人調(diào)用.next({data: ..., coffee: ...}),我們將這個(gè)對(duì)象存儲(chǔ)在本地變量results中并繼續(xù)執(zhí)行下一行代碼。
下面這段代碼完整地展示了這一行代碼完整的上下文會(huì)話:
function* handle(request) {
var results = yield getdataandlatte(request.areacode);
results.coffee.drink();
var target = mosturgentrecord(results.data);
yield updatestatus(target.id, "ready");
}
yield仍然保持著它的原始含義:暫停生成器,返回值給調(diào)用者。但是確實(shí)也發(fā)生了變化!這里的生成器期待來(lái)自調(diào)用者的非常具體的支持行為,就好像調(diào)用者是它的行政助理一樣。
普通函數(shù)則與之不同,通常更傾向于滿足調(diào)用者的需求。但是你可以借助生成器創(chuàng)造一段對(duì)話,拓展生成器與其調(diào)用者之間可能存在的關(guān)系。
這個(gè)行政助理生成器運(yùn)行器可能是什么樣的?它大可不必很復(fù)雜,就像這樣:
function rungeneratoronce(g, result) {
var status = g.next(result);
if (status.done) {
return; // phew!
}
// 生成器請(qǐng)我們?nèi)カ@取一些東西并且
// 當(dāng)我們搞定的時(shí)候再回調(diào)它
doasynchronousworkincludingespressomachineoperations(
status.value,
(error, nextresult) => rungeneratoronce(g, nextresult));
}
為了讓這段代碼運(yùn)行起來(lái),我們必須創(chuàng)建一個(gè)生成器并且運(yùn)行一次,像這樣:
rungeneratoronce(handle(request), undefined);
在之前的文章中,我一個(gè)庫(kù)的示例中提到Q.async(),在那個(gè)庫(kù)中,生成器是可以根據(jù)需要自動(dòng)運(yùn)行的異步過(guò)程。rungeneratoronce正式這樣的一個(gè)具體實(shí)現(xiàn)。事實(shí)上,生成器一般會(huì)生成Promise對(duì)象來(lái)告訴調(diào)用者要做的事情,而不是生成字符串來(lái)大聲告訴他們。
如果你已經(jīng)理解了Promise的概念,現(xiàn)在又理解了生成器的概念,你可以嘗試修改rungeneratoronce的代碼來(lái)支持Promise。這個(gè)任務(wù)不簡(jiǎn)單,但是一旦成功,你將能夠用Promise線性書(shū)寫(xiě)復(fù)雜的異步算法,而不僅僅通過(guò).then()方法或回調(diào)函數(shù)來(lái)實(shí)現(xiàn)異步功能。
如何銷(xiāo)毀生成器
你是否有看到rungeneratoronce的錯(cuò)誤處理過(guò)程?答案一定是沒(méi)有,因?yàn)樯厦娴氖纠兄苯雍雎粤隋e(cuò)誤!
是的,那樣做不好,但是如果我們想要以某種方法給生成器報(bào)告錯(cuò)誤,可以嘗試一下這個(gè)方法:當(dāng)有錯(cuò)誤產(chǎn)生時(shí),不要繼續(xù)調(diào)用generator.next(result)方法,而應(yīng)該調(diào)用generator.throw(error)方法來(lái)拋出yield表達(dá)式,進(jìn)而像.return()方法一樣終止生成器的執(zhí)行。但是如果當(dāng)前的生成暫停點(diǎn)在一個(gè)try代碼塊中,那么會(huì)catch到錯(cuò)誤并執(zhí)行finally代碼塊,生成器就恢復(fù)執(zhí)行了。
另一項(xiàng)艱巨的任務(wù)來(lái)啦,你需要修改rungeneratoronce來(lái)確保.throw()方法能夠被恰當(dāng)?shù)卣{(diào)用。請(qǐng)記住,生成器內(nèi)部拋出的異常總是會(huì)傳播到調(diào)用者。所以無(wú)論生成器是否捕獲錯(cuò)誤,generator.throw(error)都會(huì)拋出error并立即返回給你。
當(dāng)生成器執(zhí)行到一個(gè)yield表達(dá)式并暫停后可以實(shí)現(xiàn)以下功能:
- 調(diào)用generator.next(value),生成器從離開(kāi)的地方恢復(fù)執(zhí)行。
- 調(diào)用generator.return(),傳遞一個(gè)可選值,生成器只執(zhí)行finally代碼塊并不再恢復(fù)執(zhí)行。
- 調(diào)用generator.throw(error),生成器表現(xiàn)得像是yield表達(dá)式調(diào)用一個(gè)函數(shù)并拋出錯(cuò)誤。
- 或者,什么也不做,生成器永遠(yuǎn)保持凍結(jié)狀態(tài)。(是的,對(duì)于一個(gè)生成器來(lái)說(shuō),很可能執(zhí)行到一個(gè)try代碼塊,永不執(zhí)行finally代碼塊。這種狀態(tài)下的生成器可以被垃圾收集器回收。)
看起來(lái)生成器函數(shù)與普通函數(shù)的復(fù)雜度相當(dāng),只有.return()方法顯得不太一樣。
事實(shí)上,yield與函數(shù)調(diào)用有許多共通的地方。當(dāng)你調(diào)用一個(gè)函數(shù),你就暫時(shí)停止了,對(duì)不對(duì)?你調(diào)用的函數(shù)取得主導(dǎo)權(quán),它可能返回值,可能拋出錯(cuò)誤,或者永遠(yuǎn)循環(huán)下去。
結(jié)合生成器實(shí)現(xiàn)更多功能
我再展示一個(gè)特性。假設(shè)我們寫(xiě)一個(gè)簡(jiǎn)單的生成器函數(shù)聯(lián)結(jié)兩個(gè)可迭代對(duì)象:
function* concat(iter1, iter2) {
for (var value of iter1) {
yield value;
}
for (var value of iter2) {
yield value;
}
}
es6支持這樣的簡(jiǎn)寫(xiě)方式:
function* concat(iter1, iter2) {
yield* iter1;
yield* iter2;
}
普通yield表達(dá)式只生成一個(gè)值,而yield*表達(dá)式可以通過(guò)迭代器進(jìn)行迭代生成所有的值。
這個(gè)語(yǔ)法也可以用來(lái)解決另一個(gè)有趣的問(wèn)題:在生成器中調(diào)用生成器。在普通函數(shù)中,我們可以從將一個(gè)函數(shù)重構(gòu)為另一個(gè)函數(shù)并保留所有行為。很顯然我們也想重構(gòu)生成器,但我們需要一種調(diào)用提取出來(lái)的子例程的方法,我們還需要確保,子例程能夠生成之前生成的每一個(gè)值。yield*可以幫助我們實(shí)現(xiàn)這一目標(biāo)。
function* factoredoutchunkofcode() { ... }
function* refactoredfunction() {
...
yield* factoredoutchunkofcode();
...
}
考慮一下這樣一個(gè)場(chǎng)景:一個(gè)黃銅機(jī)器人將子任務(wù)委托給另一個(gè)機(jī)器人,函數(shù)對(duì)組織同步代碼來(lái)說(shuō)至關(guān)重要,所以這種思想可以使基于生成器特性的大型項(xiàng)目保持簡(jiǎn)潔有序。
模板字符串
反撇號(hào)(`)基礎(chǔ)知識(shí)
ES6引入了一種新型的字符串字面量語(yǔ)法,我們稱之為模板字符串(template strings)。除了使用反撇號(hào)字符 ` 代替普通字符串的引號(hào) ' 或 " 外,它們看起來(lái)與普通字符串并無(wú)二致。在最簡(jiǎn)單的情況下,它們與普通字符串的表現(xiàn)一致:
context.fillText(`Ceci n'est pas une chaîne.`, x, y);
但我們不能說(shuō):“原來(lái)只是被反撇號(hào)括起來(lái)的普通字符串啊”。模板字符串為JavaScript提供了簡(jiǎn)單的字符串插值功能,從此以后,你可以通過(guò)一種更加美觀、更加方便的方式向字符串中插值了。這在 Java 和 C# 中早已經(jīng)有了,不用再用 + 符號(hào)連接字符串,用起來(lái)很方便~
模板字符串的使用方式成千上萬(wàn),但最讓我暖心的是將其應(yīng)用于毫不起眼的錯(cuò)誤消息提示:
function authorize(user, action) {
if (!user.hasPrivilege(action)) {
throw new Error(
`用戶 ${user.name} 未被授權(quán)執(zhí)行 ${action} 操作。`);
}
}
在這個(gè)示例中,${user.name} 和 ${action} 被稱為模板占位符,JavaScript將把user.name和action的值插入到最終生成的字符串中,例如:用戶jorendorff未被授權(quán)打冰球。(這是真的,我還沒(méi)有獲得冰球許可證。)
到目前為止,我們所了解到的僅僅是比 + 運(yùn)算符更優(yōu)雅的語(yǔ)法,下面是你可能期待的一些特性細(xì)節(jié):
- 模板占位符中的代碼可以是任意JavaScript表達(dá)式,所以函數(shù)調(diào)用、算數(shù)運(yùn)算等這些都可以作為占位符使用,你甚至可以在一個(gè)模板字符串中嵌套另一個(gè),我稱之為模板套構(gòu)(template inception)。
- 如果這兩個(gè)值都不是字符串,可以按照常規(guī)將其轉(zhuǎn)換為字符串。例如:如果action是一個(gè)對(duì)象,將會(huì)調(diào)用它的.toString()方法將其轉(zhuǎn)換為字符串值。
- 如果你需要在模板字符串中書(shū)寫(xiě)反撇號(hào),你必須使用反斜杠將其轉(zhuǎn)義:```等價(jià)于"`"。
- 同樣地,如果你需要在模板字符串中引入字符$和{。無(wú)論你要實(shí)現(xiàn)什么樣的目標(biāo),你都需要用反斜杠轉(zhuǎn)義每一個(gè)字符:`$`和`{`。
與普通字符串不同的是,模板字符串可以多行書(shū)寫(xiě):
$("#warning").html(`
<h1>小心!>/h1>
<p>未經(jīng)授權(quán)打冰球可能受罰
將近${maxPenalty}分鐘。</p>
`);
模板字符串中所有的空格、新行、縮進(jìn),都會(huì)原樣輸出在生成的字符串中。
好啦,我說(shuō)過(guò)要讓你們輕松掌握模板字符串,從現(xiàn)在起難度會(huì)加大,你可以到此為止,去喝一杯咖啡,慢慢消化之前的知識(shí)。真的,及時(shí)回頭不是一件令人感到羞愧的事情。Lopes Gonçalves曾經(jīng)向我們證明過(guò),船只不會(huì)被海妖碾壓,也不會(huì)從地球的邊緣墜落下去,他最終跨越了赤道,但是他有繼續(xù)探索整個(gè)南半球么?并沒(méi)有,他回家了,吃了一頓豐盛的午餐,你一定不排斥這樣的感覺(jué)。
反撇號(hào)的未來(lái)
當(dāng)然,模板字符串也并非事事包攬:
- 它們不會(huì)為你自動(dòng)轉(zhuǎn)義特殊字符,為了避免跨站腳本漏洞,你應(yīng)當(dāng)像拼接普通字符串時(shí)做的那樣對(duì)非置信數(shù)據(jù)進(jìn)行特殊處理。
- 它們無(wú)法很好地與國(guó)際化庫(kù)(可以幫助你面向不同用戶提供不同的語(yǔ)言)相配合,模板字符串不會(huì)格式化特定語(yǔ)言的數(shù)字和日期,更別提同時(shí)使用不同語(yǔ)言的情況了。
- 它們不能替代模板引擎的地位,例如:Mustache、Nunjucks。
模板字符串沒(méi)有內(nèi)建循環(huán)語(yǔ)法,所以你無(wú)法通過(guò)遍歷數(shù)組來(lái)構(gòu)建類(lèi)似HTML中的表格,甚至它連條件語(yǔ)句都不支持。你當(dāng)然可以使用模板套構(gòu)(template inception)的方法實(shí)現(xiàn),但在我看來(lái)這方法略顯愚鈍啊。
不過(guò),ES6為JS開(kāi)發(fā)者和庫(kù)設(shè)計(jì)者提供了一個(gè)很好的衍生工具,你可以借助這一特性突破模板字符串的諸多限制,我們稱之為標(biāo)簽?zāi)0澹╰agged templates)。
標(biāo)簽?zāi)0宓恼Z(yǔ)法非常簡(jiǎn)單,在模板字符串開(kāi)始的反撇號(hào)前附加一個(gè)額外的標(biāo)簽即可。我們的第一個(gè)示例將添加一個(gè)SaferHTML標(biāo)簽,我們要用這個(gè)標(biāo)簽來(lái)解決上述的第一個(gè)限制:自動(dòng)轉(zhuǎn)義特殊字符。
請(qǐng)注意,ES6標(biāo)準(zhǔn)庫(kù)不提供類(lèi)似SaferHTML功能,我們將在下面自己來(lái)實(shí)現(xiàn)這個(gè)功能。
var message =
SaferHTML`<p>${bonk.sender} 向你示好。</p>`;
這里用到的標(biāo)簽是一個(gè)標(biāo)識(shí)符SaferHTML;也可以使用屬性值作為標(biāo)簽,例如:SaferHTML.escape;還可以是一個(gè)方法調(diào)用,例如:SaferHTML.escape({unicodeControlCharacters: false})。精確地說(shuō),任何ES6的成員表達(dá)式(MemberExpression)或調(diào)用表達(dá)式(CallExpression)都可作為標(biāo)簽使用。
可以看出,無(wú)標(biāo)簽?zāi)0遄址?jiǎn)化了簡(jiǎn)單字符串拼接,標(biāo)簽?zāi)0鍎t完全簡(jiǎn)化了函數(shù)調(diào)用!
上面的代碼等效于:
var message =
SaferHTML(templateData, bonk.sender);
templateData是一個(gè)不可變數(shù)組,存儲(chǔ)著模板所有的字符串部分,由JS引擎為我們創(chuàng)建。因?yàn)檎嘉环麑?biāo)簽?zāi)0宸指顬閮蓚€(gè)字符串的部分,所以這個(gè)數(shù)組內(nèi)含兩個(gè)元素,形如Object.freeze(["<p>", " has sent you a bonk.</p>"]。
(事實(shí)上,templateData中還有一個(gè)屬性,在這篇文章中我們不會(huì)用到,但是它是標(biāo)簽?zāi)0宀豢煞指畹囊画h(huán):templateData.raw,它同樣是一個(gè)數(shù)組,存儲(chǔ)著標(biāo)簽?zāi)0逯兴械淖址糠郑绻覀儾榭丛创a將會(huì)發(fā)現(xiàn),在這里是使用形如n的轉(zhuǎn)義序列分行,而在templateData中則為真正的新行,標(biāo)準(zhǔn)標(biāo)簽String.raw會(huì)用到這些原生字符串。)
如此一來(lái),SaferHTML函數(shù)就可以有成千上萬(wàn)種方法來(lái)解析字符串和占位符。
在繼續(xù)閱讀以前,可能你苦苦思索到底用SaferHTML來(lái)做什么,然后著手嘗試去實(shí)現(xiàn)它,歸根結(jié)底,它只是一個(gè)函數(shù),你可以在Firefox的開(kāi)發(fā)者控制臺(tái)里測(cè)試你的成果。
以下是一種可行的方案(在gist中查看):
function SaferHTML(templateData) {
var s = templateData[0];
for (var i = 1; i < arguments.length; i++) {
var arg = String(arguments[i]);
// 轉(zhuǎn)義占位符中的特殊字符。
s += arg.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/</g, ">");
// 不轉(zhuǎn)義模板中的特殊字符。
s += templateData[i];
}
return s;
}
通過(guò)這樣的定義,標(biāo)簽?zāi)0錝aferHTML`<p>${bonk.sender} 向你示好。</p>` 可能擴(kuò)展為字符串 "<p>ES6<3er 向你示好。</p>"。即使一個(gè)惡意命名的用戶,例如“黑客Steve<script>alert('xss');< /script>”,向其他用戶發(fā)送一條騷擾信息,無(wú)論如何這條信息都會(huì)被轉(zhuǎn)義為普通字符串,其他用戶不會(huì)受到潛在攻擊的威脅。
(順便一提,如果你感覺(jué)上述代碼中在函數(shù)內(nèi)部使用參數(shù)對(duì)象的方式令你感到枯燥乏味,不妨期待下一篇,ES6中的另一個(gè)新特性一定會(huì)讓你眼前一亮!)
僅一個(gè)簡(jiǎn)單的示例不足以說(shuō)明標(biāo)簽?zāi)0宓撵`活性,我們一起回顧下我們之前有關(guān)模板字符串限制的列表,看一下你還能做些什么不一樣的事情。
- 模板字符串不會(huì)自動(dòng)轉(zhuǎn)義特殊字符。但是正如我們看到的那樣,通過(guò)標(biāo)簽?zāi)0澹憧梢宰约簩?xiě)一個(gè)標(biāo)簽函數(shù)來(lái)解決這個(gè)問(wèn)題。
事實(shí)上,你可以做的比那更好。
站在安全角度來(lái)說(shuō),我實(shí)現(xiàn)的SaferHTML函數(shù)相當(dāng)脆弱,你需要通過(guò)多種不同的方式將HTML不同部分的特殊字符轉(zhuǎn)義,SaferHTML就無(wú)法做到全部轉(zhuǎn)義。但是稍加努力,你就可以寫(xiě)出一個(gè)更加智能的SaferHTML函數(shù),它可以針對(duì)templateData中字符串中的HTML位進(jìn)行解析,分析出哪一個(gè)占位符是純HTML;哪一個(gè)是元素內(nèi)部屬性,需要轉(zhuǎn)義'和";哪一個(gè)是URL的query字符串,需要進(jìn)行URL轉(zhuǎn)義而非HTML轉(zhuǎn)義,等等。智能SaferHTML函數(shù)可以將每個(gè)占位符都正確轉(zhuǎn)義。
HTML的解析速度很慢,這種方法聽(tīng)起來(lái)是否略顯牽強(qiáng)?幸運(yùn)的是,當(dāng)模板重新求值的時(shí)候標(biāo)簽?zāi)0宓淖址糠质遣桓淖兊摹aferHTML可以緩存所有的解析結(jié)果,來(lái)加速后續(xù)的調(diào)用。(緩存可以按照ES6的另一個(gè)特性——WeakMap的形式進(jìn)行存儲(chǔ),我們將在未來(lái)的文章中繼續(xù)深入討論。)
- 模板字符串沒(méi)有內(nèi)建的國(guó)際化特性,但是通過(guò)標(biāo)簽,我們可以添加這些功能。Jack Hsu的一篇博客文章展示了具體的實(shí)現(xiàn)過(guò)程。我謹(jǐn)在此處拋磚引玉:
i18n`Hello ${name}, you have ${amount}:c(CAD) in your bank account.`
// => Hallo Bob, Sie haben 1.234,56 $CA auf Ihrem Bankkonto.
注意觀察這個(gè)示例中的運(yùn)行細(xì)節(jié),name和amount都是JavaScript,進(jìn)行正常插值處理,但是有一段與眾不同的代碼,:c(CAD),Jack將它放入了模板的字符串部分。JavaScript理應(yīng)由JavaScript引擎進(jìn)行處理,字符串部分由Jack的 i18n標(biāo)簽進(jìn)行處理。使用者可以通過(guò)i18n的文檔了解到,:c(CAD)代表加拿大元的貨幣單位。
這就是標(biāo)簽?zāi)0宓拇蟛糠謱?shí)際應(yīng)用了。
- 模板字符串不能代替Mustache和Nunjucks,一部分原因是在模板字符串沒(méi)有內(nèi)建的循環(huán)或條件語(yǔ)句語(yǔ)法。我們一起來(lái)看如何解決這個(gè)問(wèn)題,如果JS不提供這個(gè)特性,我們就寫(xiě)一個(gè)標(biāo)簽來(lái)提供相應(yīng)支持。
// 基于純粹虛構(gòu)的模板語(yǔ)言
// ES6標(biāo)簽?zāi)0濉?/code>
var libraryHtml = hashTemplate`
<ul>
#for book in ${myBooks}
<li><i>#{book.title}</i> by #{book.author}</li>
#end
</ul>
`;
標(biāo)簽?zāi)0鍘?lái)的靈活性遠(yuǎn)不止于此,要記住,標(biāo)簽函數(shù)的參數(shù)不會(huì)自動(dòng)轉(zhuǎn)換為字符串,它們?nèi)绶祷刂狄粯樱梢允侨魏沃担瑯?biāo)簽?zāi)0迳踔敛灰欢ㄒ亲址∧憧梢杂米远x的標(biāo)簽來(lái)創(chuàng)建正則表達(dá)式、DOM樹(shù)、圖片、以promises為代表的整個(gè)異步過(guò)程、JS數(shù)據(jù)結(jié)構(gòu)、GL著色器……
標(biāo)簽?zāi)0逡蚤_(kāi)放的姿態(tài)歡迎庫(kù)設(shè)計(jì)者們來(lái)創(chuàng)建強(qiáng)有力的領(lǐng)域特定語(yǔ)言。這些語(yǔ)言可能看起來(lái)不像JS,但是它們?nèi)钥梢詿o(wú)縫嵌入到JS中并與JS的其它語(yǔ)言特性智能交互。我不知道這一特性將會(huì)帶領(lǐng)我們走向何方,但它蘊(yùn)藏著無(wú)限的可能性,這令我感到異常興奮!
我什么時(shí)候可以開(kāi)始使用這一特性?
在服務(wù)器端,io.js支持ES6的模板字符串。
在瀏覽器端,F(xiàn)irefox 34+支持模板字符串。它們由去年夏天的實(shí)習(xí)生項(xiàng)目組里的Guptha Rajagopal實(shí)現(xiàn)。模板字符串同樣在Chrome 41+中得以支持,但是IE和Safari都不支持。到目前為止,如果你想要在web端使用模板字符串的功能,你將需要Babel或Traceur協(xié)助你完成ES6到ES5的代碼轉(zhuǎn)譯,你也可以在TypeScript中立即使用這一特性。
等等——那么Markdown呢?
嗯?
哦…這是個(gè)好問(wèn)題。
(這一章節(jié)與JavaScript無(wú)關(guān),如果你不使用Markdown,可以跳過(guò)這一章。)
對(duì)于模板字符串而言,Markdown和JavaScript現(xiàn)在都使用`字符來(lái)表示一些特殊的事物。事實(shí)上,在Markdown中,反撇號(hào)用來(lái)分割在內(nèi)聯(lián)文本中間的代碼片段。
這會(huì)帶來(lái)許多問(wèn)題!如果你在Markdown中寫(xiě)這樣的文檔:
To display a message, write `alert(`hello world!`)`.
它將這樣顯示:
To display a message, write alert(hello world!).
請(qǐng)注意,輸出文本中的反撇號(hào)消失了。Markdown將所有的四個(gè)反撇號(hào)解釋為代碼分隔符并用HTML標(biāo)簽將其替換掉。
為了避免這樣的情況發(fā)生,我們要借助Markdown中的一個(gè)鮮為人知的特性,你可以使用多行反撇號(hào)作為代碼分隔符,就像這樣:
To display a message, write ``alert(`hello world!`)``.
在這個(gè)Gist有具體代碼細(xì)節(jié),它由Markdown寫(xiě)成,所以你可以直接查看源代碼。
不定參數(shù)和默認(rèn)參數(shù)
不定參數(shù)
我們通常使用可變參函數(shù)來(lái)構(gòu)造API,可變參函數(shù)可接受任意數(shù)量的參數(shù)。例如,String.prototype.concat方法就可以接受任意數(shù)量的字符串參數(shù)。ES6提供了一種編寫(xiě)可變參函數(shù)的新方式——不定參數(shù)。
我們通過(guò)一個(gè)簡(jiǎn)單的可變參數(shù)函數(shù)containsAll給大家演示不定參數(shù)的用法。函數(shù)containsAll可以檢查一個(gè)字符串中是否包含若干個(gè)子串,例如:containsAll("banana", "b", "nan")返回true,containsAll("banana", "c", "nan")返回false。
首先使用傳統(tǒng)方法來(lái)實(shí)現(xiàn)這個(gè)函數(shù):
function containsAll(haystack) {
for (var i = 1; i < arguments.length; i++) {
var needle = arguments[i];
if (haystack.indexOf(needle) === -1) {
return false;
}
}
return true;
}
在這個(gè)實(shí)現(xiàn)中,我們用到了神奇的arguments對(duì)象,它是一個(gè)類(lèi)數(shù)組對(duì)象,其中包含了傳遞給函數(shù)的所有參數(shù)。這段代碼實(shí)現(xiàn)了我們的需求,但它的可讀性卻不是最理想的。函數(shù)的參數(shù)列表中只有一個(gè)參數(shù) haystack,我們無(wú)法一眼就看出這個(gè)函數(shù)實(shí)際上接受了多個(gè)參數(shù)。另外,我們一定要注意,應(yīng)該從1開(kāi)始迭代,而不是從0開(kāi)始,因?yàn)?arguments[0]相當(dāng)于參數(shù)haystack。如果我們想要在haystack前后添加另一個(gè)參數(shù),我們一定要記得更新循環(huán)體。不定參數(shù)恰好可以解決可讀性與參數(shù)索引的問(wèn)題。下面是用ES6不定參數(shù)特性實(shí)現(xiàn)的containsAll函數(shù):
function containsAll(haystack, ...needles) {
for (var needle of needles) {
if (haystack.indexOf(needle) === -1) {
return false;
}
}
return true;
}
這一版containsAll函數(shù)與前者有相同的行為,但這一版中使用了一個(gè)特殊的...needles語(yǔ)法。我們來(lái)看一下調(diào)用 containsAll("banana", "b", "nan")之后的函數(shù)調(diào)用過(guò)程,與之前一樣,傳遞進(jìn)來(lái)的第一個(gè)參數(shù)"banana"賦值給參數(shù)haystack,needles前的省略號(hào)表明它是一個(gè)不定參數(shù),所有傳遞進(jìn)來(lái)的其它參數(shù)都被放到一個(gè)數(shù)組中,賦值給變量needles。對(duì)于我們的調(diào)用示例而言,needles被賦值為["b", "nan"],后續(xù)的函數(shù)執(zhí)行過(guò)程一如往常。(注意啦,我們已經(jīng)使用過(guò)ES6中for-of循環(huán)。)
在所有函數(shù)參數(shù)中,只有最后一個(gè)才可以被標(biāo)記為不定參數(shù)。函數(shù)被調(diào)用時(shí),不定參數(shù)前的所有參數(shù)都正常填充,任何“額外的”參數(shù)都被放進(jìn)一個(gè)數(shù)組中并賦值給不定參數(shù)。如果沒(méi)有額外的參數(shù),不定參數(shù)就是一個(gè)空數(shù)組,它永遠(yuǎn)不會(huì)是undefined。
默認(rèn)參數(shù)
通常來(lái)說(shuō),函數(shù)調(diào)用者不需要傳遞所有可能存在的參數(shù),沒(méi)有被傳遞的參數(shù)可由感知到的默認(rèn)參數(shù)進(jìn)行填充。JavaScript有嚴(yán)格的默認(rèn)參數(shù)格式,未被傳值的參數(shù)默認(rèn)為undefined。ES6引入了一種新方式,可以指定任意參數(shù)的默認(rèn)值。
下面是一個(gè)簡(jiǎn)單的示例(反撇號(hào)表示模板字符串,上周已經(jīng)討論過(guò)。):
function animalSentence(animals2="tigers", animals3="bears") {
return `Lions and ${animals2} and ${animals3}! Oh my!`;
}
默認(rèn)參數(shù)的定義形式為[param1[ = defaultValue1 ][, ..., paramN[ = defaultValueN ]]],對(duì)于每個(gè)參數(shù)而言,定義默認(rèn)值時(shí)=后的部分是一個(gè)表達(dá)式,如果調(diào)用者沒(méi)有傳遞相應(yīng)參數(shù),將使用該表達(dá)式的值作為參數(shù)默認(rèn)值。相關(guān)示例如下:
animalSentence(); // Lions and tigers and bears! Oh my!
animalSentence("elephants"); // Lions and elephants and bears! Oh my!
animalSentence("elephants", "whales"); // Lions and elephants and whales! Oh my!
默認(rèn)參數(shù)有幾個(gè)微妙的細(xì)節(jié)需要注意:
- 默認(rèn)值表達(dá)式在函數(shù)調(diào)用時(shí)自左向右求值,這一點(diǎn)與Python不同。這也意味著,默認(rèn)表達(dá)式可以使用該參數(shù)之前已經(jīng)填充好的其它參數(shù)值。舉個(gè)例子,我們優(yōu)化一下剛剛那個(gè)動(dòng)物語(yǔ)句函數(shù):
function animalSentenceFancy(animals2="tigers",
animals3=(animals2 == "bears") ? "sealions" : "bears")
{
return `Lions and ${animals2} and ${animals3}! Oh my!`;
}
現(xiàn)在,animalSentenceFancy("bears")將返回“Lions and bears and sealions. Oh my!”。
- 傳遞undefined值等效于不傳值,所以animalSentence(undefined, "unicorns")將返回“Lions and tigers and unicorns! Oh my!”。
- 沒(méi)有默認(rèn)值的參數(shù)隱式默認(rèn)為undefined,所以
function myFunc(a=42, b) {...}
是合法的,并且等效于
function myFunc(a=42, b=undefined) {...}
停止使用arguments
現(xiàn)在我們已經(jīng)看到了arguments對(duì)象可被不定參數(shù)和默認(rèn)參數(shù)完美代替,移除arguments后通常會(huì)使代碼更易于閱讀。除了破壞可讀性外,眾所周知,針對(duì)arguments對(duì)象對(duì)JavaScript虛擬機(jī)進(jìn)行的優(yōu)化會(huì)導(dǎo)致一些讓你頭疼不已的問(wèn)題。
我們期待著不定參數(shù)和默認(rèn)參數(shù)可以完全取代arguments,要實(shí)現(xiàn)這個(gè)目標(biāo),標(biāo)準(zhǔn)中增加了相應(yīng)的限制:在使用不定參數(shù)或默認(rèn)參數(shù)的函數(shù)中禁止使用arguments對(duì)象。曾經(jīng)實(shí)現(xiàn)過(guò)arguments的引擎不會(huì)立即移除對(duì)它的支持,當(dāng)然,現(xiàn)在更推薦使用不定參數(shù)和默認(rèn)參數(shù)。
瀏覽器支持
Firefox早在第15版的時(shí)候就支持了不定參數(shù)和默認(rèn)參數(shù)。
不幸的是,尚未有其它已發(fā)布的瀏覽器支持不定參數(shù)和默認(rèn)參數(shù)。V8引擎最近增添了針對(duì)不定參數(shù)的實(shí)驗(yàn)性的支持,并且有一個(gè)開(kāi)放狀態(tài)的V8 issue給實(shí)現(xiàn)默認(rèn)參數(shù)使用,JSC同樣也有一個(gè)開(kāi)放的issue來(lái)給不定參數(shù)和默認(rèn)參數(shù)使用。
Babel和Traceur編譯器都支持默認(rèn)參數(shù),所以從現(xiàn)在起就可以開(kāi)始使用。
解構(gòu) Destructuring
什么是解構(gòu)賦值?
解構(gòu)賦值允許你使用類(lèi)似數(shù)組或?qū)ο笞置媪康恼Z(yǔ)法將數(shù)組和對(duì)象的屬性賦給各種變量。這種賦值語(yǔ)法極度簡(jiǎn)潔,同時(shí)還比傳統(tǒng)的屬性訪問(wèn)方法更為清晰。
通常來(lái)說(shuō),你很可能這樣訪問(wèn)數(shù)組中的前三個(gè)元素:
var first = someArray[0];
var second = someArray[1];
var third = someArray[2];
如果使用解構(gòu)賦值的特性,將會(huì)使等效的代碼變得更加簡(jiǎn)潔并且可讀性更高:
var [first, second, third] = someArray;
SpiderMonkey(Firefox的JavaScript引擎)已經(jīng)支持解構(gòu)的大部分功能,但是仍不健全。你可以通過(guò)bug 694100跟蹤解構(gòu)和其它ES6特性在SpiderMonkey中的支持情況。
數(shù)組與迭代器的解構(gòu)
以上是數(shù)組解構(gòu)賦值的一個(gè)簡(jiǎn)單示例,其語(yǔ)法的一般形式為:
[ variable1, variable2, ..., variableN ] = array;
這將為variable1到variableN的變量賦予數(shù)組中相應(yīng)元素項(xiàng)的值。如果你想在賦值的同時(shí)聲明變量,可在賦值語(yǔ)句前加入var、let或const關(guān)鍵字,例如:
var [ variable1, variable2, ..., variableN ] = array;
let [ variable1, variable2, ..., variableN ] = array;
const [ variable1, variable2, ..., variableN ] = array;
事實(shí)上,用變量來(lái)描述并不恰當(dāng),因?yàn)槟憧梢詫?duì)任意深度的嵌套數(shù)組進(jìn)行解構(gòu):
var [foo, [[bar], baz]] = [1, [[2], 3]];
console.log(foo);
// 1
console.log(bar);
// 2
console.log(baz);
// 3
此外,你可以在對(duì)應(yīng)位留空來(lái)跳過(guò)被解構(gòu)數(shù)組中的某些元素:
var [,,third] = ["foo", "bar", "baz"];
console.log(third);
// "baz"
而且你還可以通過(guò)“不定參數(shù)”模式捕獲數(shù)組中的所有尾隨元素:
var [head, ...tail] = [1, 2, 3, 4];
console.log(tail);
// [2, 3, 4]
當(dāng)訪問(wèn)空數(shù)組或越界訪問(wèn)數(shù)組時(shí),對(duì)其解構(gòu)與對(duì)其索引的行為一致,最終得到的結(jié)果都是:undefined。
console.log([][0]);
// undefined
var [missing] = [];
console.log(missing);
// undefined
請(qǐng)注意,數(shù)組解構(gòu)賦值的模式同樣適用于任意迭代器:
function* fibs() {
var a = 0;
var b = 1;
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
var [first, second, third, fourth, fifth, sixth] = fibs();
console.log(sixth);
// 5
對(duì)象的解構(gòu)
通過(guò)解構(gòu)對(duì)象,你可以把它的每個(gè)屬性與不同的變量綁定,首先指定被綁定的屬性,然后緊跟一個(gè)要解構(gòu)的變量。
var robotA = { name: "Bender" };
var robotB = { name: "Flexo" };
var { name: nameA } = robotA;
var { name: nameB } = robotB;
console.log(nameA);
// "Bender"
console.log(nameB);
// "Flexo"
當(dāng)屬性名與變量名一致時(shí),可以通過(guò)一種實(shí)用的句法簡(jiǎn)寫(xiě):
var { foo, bar } = { foo: "lorem", bar: "ipsum" };
console.log(foo);
// "lorem"
console.log(bar);
// "ipsum"
與數(shù)組解構(gòu)一樣,你可以隨意嵌套并進(jìn)一步組合對(duì)象解構(gòu):
var complicatedObj = {
arrayProp: [
"ZApp",
{ second: "Brannigan" }
]
};
var { arrayProp: [first, { second }] } = complicatedObj;
console.log(first);
// "Zapp"
console.log(second);
// "Brannigan"
當(dāng)你解構(gòu)一個(gè)未定義的屬性時(shí),得到的值為undefined:
var { missing } = {};
console.log(missing);
// undefined
請(qǐng)注意,當(dāng)你解構(gòu)對(duì)象并賦值給變量時(shí),如果你已經(jīng)聲明或不打算聲明這些變量(亦即賦值語(yǔ)句前沒(méi)有l(wèi)et、const或var關(guān)鍵字),你應(yīng)該注意這樣一個(gè)潛在的語(yǔ)法錯(cuò)誤:
{ blowUp } = { blowUp: 10 };
// Syntax error 語(yǔ)法錯(cuò)誤
為什么會(huì)出錯(cuò)?這是因?yàn)镴avaScript語(yǔ)法通知解析引擎將任何以{開(kāi)始的語(yǔ)句解析為一個(gè)塊語(yǔ)句(例如,{console}是一個(gè)合法塊語(yǔ)句)。解決方案是將整個(gè)表達(dá)式用一對(duì)小括號(hào)包裹:
({ safe } = {});
// No errors 沒(méi)有語(yǔ)法錯(cuò)誤
解構(gòu)值不是對(duì)象、數(shù)組或迭代器
當(dāng)你嘗試解構(gòu)null或undefined時(shí),你會(huì)得到一個(gè)類(lèi)型錯(cuò)誤:
var {blowUp} = null;
// TypeError: null has no properties(null沒(méi)有屬性)
然而,你可以解構(gòu)其它原始類(lèi)型,例如:布爾值、數(shù)值、字符串,但是你將得到undefined:
var {wtf} = NaN;
console.log(wtf);
// undefined
你可能對(duì)此感到意外,但經(jīng)過(guò)進(jìn)一步審查你就會(huì)發(fā)現(xiàn),原因其實(shí)非常簡(jiǎn)單。當(dāng)使用對(duì)象賦值模式時(shí),被解構(gòu)的值需要被強(qiáng)制轉(zhuǎn)換為對(duì)象。大多數(shù)類(lèi)型都可以被轉(zhuǎn)換為對(duì)象,但null和undefined卻無(wú)法進(jìn)行轉(zhuǎn)換。當(dāng)使用數(shù)組賦值模式時(shí),被解構(gòu)的值一定要包含一個(gè)迭代器。
默認(rèn)值
當(dāng)你要解構(gòu)的屬性未定義時(shí)你可以提供一個(gè)默認(rèn)值:
var [missing = true] = [];
console.log(missing);
// true
var { message: msg = "Something went wrong" } = {};
console.log(msg);
// "Something went wrong"
var { x = 3 } = {};
console.log(x);
// 3
(譯者按:Firefox目前只實(shí)現(xiàn)了這個(gè)特性的前兩種情況,第三種尚未實(shí)現(xiàn)。詳情查看bug 932080。)
解構(gòu)的實(shí)際應(yīng)用
函數(shù)參數(shù)定義
作 為開(kāi)發(fā)者,我們需要實(shí)現(xiàn)設(shè)計(jì)良好的API,通常的做法是為函數(shù)為函數(shù)設(shè)計(jì)一個(gè)對(duì)象作為參數(shù),然后將不同的實(shí)際參數(shù)作為對(duì)象屬性,以避免讓API使用者記住 多個(gè)參數(shù)的使用順序。我們可以使用解構(gòu)特性來(lái)避免這種問(wèn)題,當(dāng)我們想要引用它的其中一個(gè)屬性時(shí),大可不必反復(fù)使用這種單一參數(shù)對(duì)象。
function removeBreakpoint({ url, line, column }) {
// ...
}
這是一段來(lái)自Firefox開(kāi)發(fā)工具JavaScript調(diào)試器(同樣使用JavaScript實(shí)現(xiàn)——沒(méi)錯(cuò),就是這樣!)的代碼片段,它看起來(lái)非常簡(jiǎn)潔,我們會(huì)發(fā)現(xiàn)這種代碼模式特別討喜。
配置對(duì)象參數(shù)
延伸一下之前的示例,我們同樣可以給需要解構(gòu)的對(duì)象屬性賦予默認(rèn)值。當(dāng)我們構(gòu)造一個(gè)提供配置的對(duì)象,并且需要這個(gè)對(duì)象的屬性攜帶默認(rèn)值時(shí),解構(gòu)特性就派上用場(chǎng)了。舉個(gè)例子,jQuery的ajax函數(shù)使用一個(gè)配置對(duì)象作為它的第二參數(shù),我們可以這樣重寫(xiě)函數(shù)定義:
jQuery.ajax = function (url, {
async = true,
beforeSend = noop,
cache = true,
complete = noop,
crossDomain = false,
global = true,
// ... 更多配置
}) {
// ... do stuff
};
如此一來(lái),我們可以避免對(duì)配置對(duì)象的每個(gè)屬性都重復(fù)var foo = config.foo || theDefaultFoo;這樣的操作。
(編者按:不幸的是,對(duì)象的默認(rèn)值簡(jiǎn)寫(xiě)語(yǔ)法仍未在Firefox中實(shí)現(xiàn),我知道,上一個(gè)編者按后的幾個(gè)段落講解的就是這個(gè)特性。點(diǎn)擊bug 932080查看最新詳情。)
與ES6迭代器協(xié)議協(xié)同使用
ECMAScript 6中定義了一個(gè)迭代器協(xié)議,我們?cè)凇渡钊霚\出ES6(二):迭代器和for-of循環(huán)》中已經(jīng)詳細(xì)解析過(guò)。當(dāng)你迭代Maps(ES6標(biāo)準(zhǔn)庫(kù)中新加入的一種對(duì)象)后,你可以得到一系列形如[key, value]的鍵值對(duì),我們可將這些鍵值對(duì)解構(gòu),更輕松地訪問(wèn)鍵和值:
var map = new Map();
map.set(window, "the global");
map.set(document, "the document");
for (var [key, value] of map) {
console.log(key + " is " + value);
}
// "[object Window] is the global"
// "[object HTMLDocument] is the document"
只遍歷鍵:
for (var [key] of map) {
// ...
}
或只遍歷值:
for (var [,value] of map) {
// ...
}
多重返回值
JavaScript語(yǔ)言中尚未整合多重返回值的特性,但是無(wú)須多此一舉,因?yàn)槟阕约壕涂梢苑祷匾粋€(gè)數(shù)組并將結(jié)果解構(gòu):
function returnMultipleValues() {
return [1, 2];
}
var [foo, bar] = returnMultipleValues();
或者,你可以用一個(gè)對(duì)象作為容器并為返回值命名:
function returnMultipleValues() {
return {
foo: 1,
bar: 2
};
}
var { foo, bar } = returnMultipleValues();
這兩個(gè)模式都比額外保存一個(gè)臨時(shí)變量要好得多。
function returnMultipleValues() {
return {
foo: 1,
bar: 2
};
}
var temp = returnMultipleValues();
var foo = temp.foo;
var bar = temp.bar;
或者使用CPS變換:
function returnMultipleValues(k) {
k(1, 2);
}
returnMultipleValues((foo, bar) => ...);
使用解構(gòu)導(dǎo)入部分CommonJS模塊
你是否尚未使用ES6模塊?還用著CommonJS的模塊呢吧!沒(méi)問(wèn)題,當(dāng)我們導(dǎo)入CommonJS模塊X時(shí),很可能在模塊X中導(dǎo)出了許多你根本沒(méi)打算用的函數(shù)。通過(guò)解構(gòu),你可以顯式定義模塊的一部分來(lái)拆分使用,同時(shí)還不會(huì)污染你的命名空間:
const { SourceMapConsumer, SourceNode } = require("source-map");
(如果你使用ES6模塊,你一定知道在import聲明中有一個(gè)相似的語(yǔ)法。)
正如你所見(jiàn),解構(gòu)在許多獨(dú)立小場(chǎng)景中非常實(shí)用。在Mozilla我們已經(jīng)積累了許多有關(guān)解構(gòu)的使用經(jīng)驗(yàn)。十年前,Lars Hansen在Opera中引入了JS解構(gòu)特性,Brendan Eich隨后就給Firefox也增加了相應(yīng)的支持,移植時(shí)版本為Firefox 2。所以我們可以肯定,漸漸地,你會(huì)在每天使用的語(yǔ)言中加入解構(gòu)這個(gè)新特性,它可以讓你的代碼變得更加精簡(jiǎn)整潔。
箭頭函數(shù) Arrow Functions
箭頭符號(hào)在JavaScript誕生時(shí)就已經(jīng)存在,當(dāng)初第一個(gè)JavaScript教程曾建議在HTML注釋內(nèi)包裹行內(nèi)腳本,這樣可以避免不支持JS的瀏覽器誤將JS代碼顯示為文本。你會(huì)寫(xiě)這樣的代碼:
<script language="javascript">
<!--
document.bgColor = "brown"; // red
// -->
</script>
老式瀏覽器會(huì)將這段代碼解析為兩個(gè)不支持的標(biāo)簽和一條注釋,只有新式瀏覽器才能識(shí)別出其中的JS代碼。
為了支持這種奇怪的hack方式,瀏覽器中的JavaScript引擎將<!-- 這四個(gè)字符解析為單行注釋的起始部分,我沒(méi)開(kāi)玩笑,這自始至終就是語(yǔ)言的一部分,直到現(xiàn)在仍然有效,這種注釋符號(hào)不僅出現(xiàn)<script>標(biāo)簽后的首行,在JS代碼的每個(gè)角落你都有可能見(jiàn)到它,甚至在Node中也是如此。
碰巧,這種注釋風(fēng)格首次在ES6中被標(biāo)準(zhǔn)化了,但在新標(biāo)準(zhǔn)中箭頭被用來(lái)做其它事情。
箭頭序列 –—> 同樣是單行注釋的一部分。古怪的是,在HTML中-->之前的字符是注釋的一部分,而在JS中-->之后的部分才是注釋。
你一定感到陌生的是,只有當(dāng)箭頭在行首時(shí)才會(huì)注釋當(dāng)前行。這是因?yàn)樵谄渌舷挛闹校?->是一個(gè)JS運(yùn)算符:“趨向于”運(yùn)算符!
function countdown(n) {
while (n --> 0) // "n goes to zero"
alert(n);
blastoff();
}
上面這段代碼可以正常運(yùn)行,循環(huán)會(huì)一直重復(fù)直到n趨于0,這當(dāng)然不是ES6中的新特性,它只不過(guò)是將兩個(gè)你早已熟悉的特性通過(guò)一些誤導(dǎo)性的手段結(jié)合在一起。你能理解么?通常來(lái)說(shuō),類(lèi)似這種謎團(tuán)都可以在Stack Overflow上找到答案。
當(dāng)然,同樣地,小于等于操作符<=也形似箭頭,你可以在JS代碼、隱藏的圖片樣式中找到更多類(lèi)似的箭頭,但是我們就不繼續(xù)尋找了,你應(yīng)該注意到我們漏掉了一種特殊的箭頭。
<!--
單行注釋
-->
“趨向于”操作符
<=
小于等于
=>
這又是什么?
=>到底是什么?我們今天就來(lái)一探究竟。
首先,我們談?wù)撘恍┯嘘P(guān)函數(shù)的事情。
函數(shù)表達(dá)式無(wú)處不在
JavaScript中有一個(gè)有趣的特性,無(wú)論何時(shí),當(dāng)你需要一個(gè)函數(shù)時(shí),你都可以在想添加的地方輸入這個(gè)函數(shù)。
舉個(gè)例子,假設(shè)你嘗試告訴瀏覽器用戶點(diǎn)擊一個(gè)特定按鈕后的行為,你會(huì)這樣寫(xiě):
$("#confetti-btn").click(
jQuery的.click()方法接受一個(gè)參數(shù):一個(gè)函數(shù)。沒(méi)問(wèn)題,你可以在這里輸入一個(gè)函數(shù):
$("#confetti-btn").click(function (event) {
playTrumpet();
fireConfettiCannon();
});
對(duì)于現(xiàn)在的我們來(lái)說(shuō),寫(xiě)出這樣的代碼相當(dāng)自然,而回憶起在這種編程方式流行之前,這種寫(xiě)法相對(duì)陌生一些,許多語(yǔ)言中都沒(méi)有這種特性。1958年,Lisp首先支持函數(shù)表達(dá)式,也支持調(diào)用lambda函數(shù),而C++,Python、C#以及Java在隨后的多年中一直不支持這樣的特性。
現(xiàn)在截然不同,所有的四種語(yǔ)言都已支持lambda函數(shù),更新出現(xiàn)的語(yǔ)言普遍都支持內(nèi)建的lambda函數(shù)。我們必須要感謝JavaScript和早期的JavaScript程序員,他們勇敢地構(gòu)建了重度依賴lambda函數(shù)的庫(kù),讓這種特性被廣泛接受。
令人傷感的是,隨后在所有我提及的語(yǔ)言中,只有JavaScript的lambda的語(yǔ)法最終變得冗長(zhǎng)乏味。
// 六種語(yǔ)言中的簡(jiǎn)單函數(shù)示例
function (a) { return a > 0; } // JS
[](int a) { return a > 0; } // C++
(lambda (a) (> a 0)) ;; Lisp
lambda a: a > 0 # Python
a => a > 0 // C#
a -> a > 0 // Java
箭袋中的新羽
ES6中引入了一種編寫(xiě)函數(shù)的新語(yǔ)法
// ES5
var selected = allJobs.filter(function (job) {
return job.isSelected();
});
// ES6
var selected = allJobs.filter(job => job.isSelected());
當(dāng)你只需要一個(gè)只有一個(gè)參數(shù)的簡(jiǎn)單函數(shù)時(shí),可以使用新標(biāo)準(zhǔn)中的箭頭函數(shù),它的語(yǔ)法非常簡(jiǎn)單:標(biāo)識(shí)符=>表達(dá)式。你無(wú)需輸入function和return,一些小括號(hào)、大括號(hào)以及分號(hào)也可以省略。
(我個(gè)人對(duì)于這個(gè)特性非常感激,不再需要輸入function這幾個(gè)字符對(duì)我而言至關(guān)重要,因?yàn)槲铱偸遣豢杀苊獾劐e(cuò)誤寫(xiě)成functoin,然后我就不得不回過(guò)頭改正它。)
如果要寫(xiě)一個(gè)接受多重參數(shù)(也可能沒(méi)有參數(shù),或者是不定參數(shù)、默認(rèn)參數(shù)、參數(shù)解構(gòu))的函數(shù),你需要用小括號(hào)包裹參數(shù)list。
// ES5
var total = values.reduce(function (a, b) {
return a + b;
}, 0);
// ES6
var total = values.reduce((a, b) => a + b, 0);
我認(rèn)為這看起來(lái)酷斃了。
正如你使用類(lèi)似Underscore.js和Immutable.js這樣的庫(kù)提供的函數(shù)工具,箭頭函數(shù)運(yùn)行起來(lái)同樣美不可言。事實(shí)上,Immutable的文檔中的示例全都由ES6寫(xiě)成,其中的許多特性已經(jīng)用上了箭頭函數(shù)。
那么不是非常函數(shù)化的情況又如何呢?除表達(dá)式外,箭頭函數(shù)還可以包含一個(gè)塊語(yǔ)句。回想一下我們之前的示例:
// ES5
$("#confetti-btn").click(function (event) {
playTrumpet();
fireConfettiCannon();
});
這是它們?cè)贓S6中看起來(lái)的樣子:
// ES6
$("#confetti-btn").click(event => {
playTrumpet();
fireConfettiCannon();
});
這是一個(gè)微小的改進(jìn),對(duì)于使用了Promises的代碼來(lái)說(shuō)箭頭函數(shù)的效果可以變得更加戲劇性,}).then(function (result) { 這樣的一行代碼可以堆積起來(lái)。
注意,使用了塊語(yǔ)句的箭頭函數(shù)不會(huì)自動(dòng)返回值,你需要使用return語(yǔ)句將所需值返回。
小提示:當(dāng)使用箭頭函數(shù)創(chuàng)建普通對(duì)象時(shí),你總是需要將對(duì)象包裹在小括號(hào)里。
// 為與你玩耍的每一個(gè)小狗創(chuàng)建一個(gè)新的空對(duì)象
var chewToys = puppies.map(puppy => {}); // 這樣寫(xiě)會(huì)報(bào)Bug!
var chewToys = puppies.map(puppy => ({})); //
用小括號(hào)包裹空對(duì)象就可以了。
不幸的是,一個(gè)空對(duì)象{}和一個(gè)空的塊{}看起來(lái)完全一樣。ES6中的規(guī)則是,緊隨箭頭的{被解析為塊的開(kāi)始,而不是對(duì)象的開(kāi)始。因此,puppy => {}這段代碼就被解析為沒(méi)有任何行為并返回undefined的箭頭函數(shù)。
更令人困惑的是,你的JavaScript引擎會(huì)將類(lèi)似{key: value}的對(duì)象字面量解析為一個(gè)包含標(biāo)記語(yǔ)句的塊。幸運(yùn)的是,{是唯一一個(gè)有歧義的字符,所以用小括號(hào)包裹對(duì)象字面量是唯一一個(gè)你需要牢記的小竅門(mén)。
這個(gè)函數(shù)的this值是什么呢?
普通function函數(shù)和箭頭函數(shù)的行為有一個(gè)微妙的區(qū)別,箭頭函數(shù)沒(méi)有它自己的this值,箭頭函數(shù)內(nèi)的this值繼承自外圍作用域。
在我們嘗試說(shuō)明這個(gè)問(wèn)題前,先一起回顧一下。
JavaScript中的this是如何工作的?它的值從哪里獲取?這些問(wèn)題的答案可都不簡(jiǎn)單,如果你對(duì)此倍感清晰,一定因?yàn)槟汩L(zhǎng)時(shí)間以來(lái)一直在處理類(lèi)似的問(wèn)題。
這個(gè)問(wèn)題經(jīng)常出現(xiàn)的其中一個(gè)原因是,無(wú)論是否需要,function函數(shù)總會(huì)自動(dòng)接收一個(gè)this值。你是否寫(xiě)過(guò)這樣的hack代碼:
{
...
addAll: function addAll(pieces) {
var self = this;
_.each(pieces, function (piece) {
self.add(piece);
});
},
...
}
在這里,你希望在內(nèi)層函數(shù)里寫(xiě)的是this.add(piece),不幸的是,內(nèi)層函數(shù)并未從外層函數(shù)繼承this的值。在內(nèi)層函數(shù)里,this會(huì)是window或undefined,臨時(shí)變量self用來(lái)將外部的this值導(dǎo)入內(nèi)部函數(shù)。(另一種方式是在內(nèi)部函數(shù)上執(zhí)行.bind(this),兩種方法都不甚美觀。)
在ES6中,不需要再hackthis了,但你需要遵循以下規(guī)則:
- 通過(guò)object.method()語(yǔ)法調(diào)用的方法使用非箭頭函數(shù)定義,這些函數(shù)需要從調(diào)用者的作用域中獲取一個(gè)有意義的this值。
- 其它情況全都使用箭頭函數(shù)。
// ES6
{
...
addAll: function addAll(pieces) {
_.each(pieces, piece => this.add(piece));
},
...
}
在ES6的版本中,注意addAll方法從它的調(diào)用者處獲取了this值,內(nèi)部函數(shù)是一個(gè)箭頭函數(shù),所以它繼承了外圍作用域的this值。
超贊的是,在ES6中你可以用更簡(jiǎn)潔的方式編寫(xiě)對(duì)象字面量中的方法,所以上面這段代碼可以簡(jiǎn)化成:
// ES6的方法語(yǔ)法
{
...
addAll(pieces) {
_.each(pieces, piece => this.add(piece));
},
...
}
在方法和箭頭函數(shù)之間,我再也不會(huì)錯(cuò)寫(xiě)functoin了,這真是一個(gè)絕妙的設(shè)計(jì)思想!
箭頭函數(shù)與非箭頭函數(shù)間還有一個(gè)細(xì)微的區(qū)別,箭頭函數(shù)不會(huì)獲取它們自己的arguments對(duì)象。誠(chéng)然,在ES6中,你可能更多地會(huì)使用不定參數(shù)和默認(rèn)參數(shù)值這些新特性。
借助箭頭函數(shù)洞悉計(jì)算機(jī)科學(xué)的風(fēng)塵往事
我們已經(jīng)討論了許多箭頭函數(shù)的實(shí)際用例,它還有一種可能的使用方法:將ES6箭頭函數(shù)作為一個(gè)學(xué)習(xí)工具,來(lái)深入挖掘計(jì)算的本質(zhì),是否實(shí)用,終將取決于你自己。
1936年,Alonzo Church和Alan Turing各自開(kāi)發(fā)了強(qiáng)大的計(jì)算數(shù)學(xué)模型,圖靈將他的模型稱為a-machines,但是每一個(gè)人都稱其為圖靈機(jī)。Church寫(xiě)的是函數(shù)模型,他的模型被稱為lambda演算(λ-calculus)。這一成果也被Lisp借鑒,用LAMBDA來(lái)指示函數(shù),這也是為何我們現(xiàn)在將函數(shù)表達(dá)式稱為lambda函數(shù)。
但什么是Lambda演算呢?“計(jì)算模型”又意味著什么呢?
用幾句話解釋清楚很難,但是我會(huì)努力闡釋:lambda演算是第一代編程語(yǔ)言的一種形式,但畢竟存儲(chǔ)程序計(jì)算機(jī)在十幾二十年后才誕生,所以它原本不是為編程語(yǔ)言設(shè)計(jì)的,而是為了表達(dá)任意你想到的計(jì)算問(wèn)題設(shè)計(jì)的一種極度簡(jiǎn)化的純數(shù)學(xué)思想的語(yǔ)言。Church希望用這個(gè)模型來(lái)證明普遍意義的計(jì)算。
最終他發(fā)現(xiàn),在他的系統(tǒng)中只需要一件東西:函數(shù)。
這種聲明方式無(wú)與倫比,不借助對(duì)象、數(shù)組、數(shù)字、if語(yǔ)句、while循環(huán)、分號(hào)、賦值、邏輯運(yùn)算符甚或是事件循環(huán),只須使用函數(shù)就可以從0開(kāi)始重建JavaScript能實(shí)現(xiàn)的每一種計(jì)算。
這是用Church的lambda標(biāo)記寫(xiě)出來(lái)的數(shù)學(xué)家風(fēng)格的“程序”示例:
fix = λf.(λx.f(λv.x(x)(v)))(λx.f(λv.x(x)(v)))
等效的JavaScript函數(shù)是這樣的:
var fix = f => (x => f(v => x(x)(v)))
(x => f(v => x(x)(v)));
所以,在JavaScript中實(shí)現(xiàn)了一個(gè)可以運(yùn)行的lambda演算,它根植于這門(mén)語(yǔ)言中。
Alonzo Church和lambda演算后繼研究者們的故事,以及它是如何潛移默化地入駐每一門(mén)主流編程語(yǔ)言的,已經(jīng)遠(yuǎn)超本文的討論范圍。但是如果你對(duì)計(jì)算機(jī)科學(xué) 的奠基感興趣,或者你只是對(duì)一門(mén)只用函數(shù)就可以做許多類(lèi)似循環(huán)和遞歸這樣的事情的語(yǔ)言倍感興趣,你可以在一個(gè)下雨的午后深入邱奇數(shù)(Church numerals)和不動(dòng)點(diǎn)組合子(Fixed-point combinator),在你的Firefox控制臺(tái)或Scratchpad中仔細(xì)研究一番。結(jié)合ES6的箭頭函數(shù)以及其它強(qiáng)大的功能,JavaScript稱得上是一門(mén)探索lambda演算的最好的語(yǔ)言。
我何時(shí)可以使用箭頭函數(shù)?
早在2013年,我就在Firefox中實(shí)現(xiàn)了ES6箭頭函數(shù)的功能,Jan de Mooij為其優(yōu)化加快了執(zhí)行速度。感謝Tooru Fujisawa以及ziyunfei(譯者注:中國(guó)開(kāi)發(fā)者,為Mozilla作了許多貢獻(xiàn))后續(xù)打的補(bǔ)丁。
微軟Edge預(yù)覽版中也實(shí)現(xiàn)了箭頭函數(shù)的功能,如果你想立即在你的Web項(xiàng)目中使用箭頭函數(shù),可以使用Babel、Traceur或TypeScript,這三個(gè)工具均已實(shí)現(xiàn)相關(guān)功能。
Symbols
你是否知道ES6中的Symbols是什么,它有什么作用呢?我相信你很可能不知道,那就讓我們一探究竟!
Symbols并非用來(lái)指代某種Logo。
它們也不是可以用作代碼的小圖標(biāo)。
它們不是代替其它東西的文學(xué)手法。
它們更不可能被用來(lái)指代諧音詞Cymbals(鐃鈸)。
(編程的時(shí)候最好不要演奏鐃鈸,它們太過(guò)吵鬧,很可能導(dǎo)致你的程序崩潰。)
那么,Symbols到底是什么呢?
它是JavaScript的第七種原始類(lèi)型
1997年JavaScript首次被標(biāo)準(zhǔn)化,那時(shí)只有六種原始類(lèi)型,在ES6以前,JS程序中使用的每一個(gè)值都是以下幾種類(lèi)型之一:
- Undefined 未定義
- Null 空值
- Boolean 布爾類(lèi)型
- Number 數(shù)字類(lèi)型
- String 字符串類(lèi)型
- Object 對(duì)象類(lèi)型
每種類(lèi)型都是多個(gè)值的集合,前五個(gè)集合是有限的。布爾類(lèi)型只有兩個(gè)值,true和false,不會(huì)再創(chuàng)造第三種布爾值;數(shù)字類(lèi)型和字符串類(lèi)型的值更多,標(biāo)準(zhǔn)指明一共有18,437,736,874,454,810,627種不同的數(shù)字(包括NaN, 亦即“Not a Number”的縮寫(xiě),代表非數(shù)字),可能存在的字符串類(lèi)型的值擁有無(wú)以匹敵的數(shù)量,我估算了一下大約是 (2144,115,188,075,855,872 − 1) ÷ 65,535種……當(dāng)然,我很可能得出了一個(gè)錯(cuò)誤的答案,但字符串類(lèi)型值的集合一定是有限的。
然而,對(duì)象類(lèi)型值的集合是無(wú)限的。每一個(gè)對(duì)象都像珍貴的雪花一樣獨(dú)一無(wú)二,每一次你打開(kāi)一個(gè)Web頁(yè)面,都會(huì)創(chuàng)建一堆對(duì)象。
ES6新特性中的symbol也是值,但它不是字符串,也不是對(duì)象,而是是全新的——第七種類(lèi)型的原始值。
讓我們一起探討一下symbol的實(shí)際應(yīng)用場(chǎng)景。
從一個(gè)簡(jiǎn)單的布爾類(lèi)型出發(fā)
有時(shí)候你可以非常輕松地將別人的外部數(shù)據(jù)存儲(chǔ)到一個(gè)JavaScript對(duì)象中。
舉 個(gè)例子,假設(shè)你正在寫(xiě)一個(gè)JS庫(kù),可以通過(guò)CSS transitions使DOM元素在屏幕上移動(dòng)。你可能會(huì)注意到,當(dāng)你嘗試在一個(gè)div元素上同時(shí)應(yīng)用多重CSS transitions時(shí)并不會(huì)生效。實(shí)際效果是丑陋而又不連續(xù)的“跳閃”。你認(rèn)為可以修復(fù)這個(gè)問(wèn)題,但前提是你需要一種發(fā)現(xiàn)給定元素是否已經(jīng)移動(dòng)過(guò)的方 法。
應(yīng)當(dāng)如何解決這個(gè)問(wèn)題呢?
一種方法是,用CSS API來(lái)告訴瀏覽器元素是否正在移動(dòng),但這樣簡(jiǎn)直小題大做。在元素移動(dòng)的第一時(shí)間內(nèi)你的庫(kù)就應(yīng)該記錄下移動(dòng)的狀態(tài),所以它自然知道元素正在移動(dòng)。
你真正想要的是一種持續(xù)跟蹤某個(gè)元素正在移動(dòng)的方法。你可以維護(hù)一個(gè)數(shù)組,記錄所有正在移動(dòng)的元素,每當(dāng)你的庫(kù)被調(diào)用來(lái)移動(dòng)某個(gè)元素時(shí),你可以檢索數(shù)組來(lái)查看元素是否已經(jīng)存在,亦即它是否正在移動(dòng)中。
當(dāng)然,如果數(shù)組非常大的話,線性搜索將會(huì)非常緩慢。
實(shí)際上你只想為元素設(shè)置一個(gè)標(biāo)記:
if (element.isMoving) {
smoothAnimations(element);
}
element.isMoving = true;
這樣也會(huì)有一些潛在的問(wèn)題,事實(shí)上,你的代碼很可能不是唯一一段操作DOM的代碼。
- 你創(chuàng)建的屬性很可能影響到其它使用了for-in或Object.keys()的代碼。
- 一些聰明的庫(kù)作者可能已經(jīng)考慮并使用了這項(xiàng)技術(shù),這樣一來(lái)你的庫(kù)就會(huì)與已有的庫(kù)產(chǎn)生某些沖突
- 當(dāng)然,很可能你比他們更聰明,你先采用了這項(xiàng)技術(shù),但是他們的庫(kù)仍然無(wú)法與你的庫(kù)默契配合。
- 標(biāo)準(zhǔn)委員會(huì)可能決定為所有的元素增加一個(gè).isMoving()方法,到那時(shí)你需要重寫(xiě)相關(guān)邏輯,必定會(huì)有深深的挫敗感。
當(dāng)然你可以選擇一個(gè)乏味而愚蠢的命名(其他人根本不會(huì)想用的那些名稱)來(lái)解決最后的三個(gè)問(wèn)題:
if (element.__$jorendorff_animation_library$PLEASE_DO_NOT_USE_THIS_PROPERTY$isMoving__) {
smoothAnimations(element);
}
element.__$jorendorff_animation_library$PLEASE_DO_NOT_USE_THIS_PROPERTY$isMoving__ = true;
這只會(huì)造成無(wú)畏的眼疲勞。
借助于密碼學(xué),你可以生成一個(gè)唯一的屬性名稱:
// 獲取1024個(gè)Unicode字符的無(wú)意義命名
var isMoving = SecureRandom.generateName();
...
if (element[isMoving]) {
smoothAnimations(element);
}
element[isMoving] = true;
object[name]語(yǔ)法允許你使用幾乎任何字符串作為屬性名稱。所以這個(gè)方法行之有效:沖突幾乎是不可能的,并且你的代碼看起來(lái)也很簡(jiǎn)潔。
但是這也將帶來(lái)不良的調(diào)試體驗(yàn)。每當(dāng)你在控制臺(tái)輸出(console.log())包含那個(gè)屬性的元素時(shí),你將會(huì)看到一堆巨大的字符串垃圾。假使你需要比這多得多的類(lèi)似屬性呢?你如何保持它們整齊劃一?每當(dāng)你重載的時(shí)候它們的命名甚至都不一樣!
為什么這個(gè)問(wèn)題如此困難?我們只想要一個(gè)小小的布爾值啊!
symbol是最終的解決方案
symbol是程序創(chuàng)建并且可以用作屬性鍵的值,并且它能避免命名沖突的風(fēng)險(xiǎn)。
var mySymbol = Symbol();
調(diào)用Symbol()創(chuàng)建一個(gè)新的symbol,它的值與其它任何值皆不相等。
字符串或數(shù)字可以作為屬性的鍵,symbol也可以,它不等同于任何字符串,因而這個(gè)以symbol為鍵的屬性可以保證不與任何其它屬性產(chǎn)生沖突。
obj[mySymbol] = "ok!"; // 保證不會(huì)沖突
console.log(obj[mySymbol]); // ok!
想要在上述討論的場(chǎng)景中使用symbol,你可以這樣做:
// 創(chuàng)建一個(gè)獨(dú)一無(wú)二的symbol
var isMoving = Symbol("isMoving");
...
if (element[isMoving]) {
smoothAnimations(element);
}
element[isMoving] = true;
有關(guān)這段代碼的一些解釋:
- Symbol("isMoving")中的isMoving被稱作描述。你可以通過(guò)console.log()將它打印出來(lái),對(duì)調(diào)試非常有幫助;你也可以用.toString()方法將它轉(zhuǎn)換為字符串呈現(xiàn);它也可以被用在錯(cuò)誤信息中。
- element[isMoving]被稱作一個(gè)以symbol為鍵(symbol-keyed)的屬性。簡(jiǎn)而言之,它的名字是symbol而不是一個(gè)字符串。除此之外,它與一個(gè)普通的屬性沒(méi)有什么區(qū)別。
- 以symbol為鍵的屬性屬性與數(shù)組元素類(lèi)似,不能被類(lèi)似obj.name的點(diǎn)號(hào)法訪問(wèn),你必須使用方括號(hào)訪問(wèn)這些屬性。
- 如果你已經(jīng)得到了symbol,那么訪問(wèn)一個(gè)以symbol為鍵的屬性同樣簡(jiǎn)單,以上的示例很好地展示了如何獲取element[isMoving]的值以及如何為它賦值。如果我們需要,可以查看屬性是否存在:if (isMoving in element),也可以刪除屬性:delete element[isMoving]。
- 另一方面,只有當(dāng)isMoving在當(dāng)前作用域中時(shí)才會(huì)生效。這是symbol的弱封裝機(jī)制:模塊創(chuàng)建了幾個(gè)symbol,可以在任意對(duì)象上使用,無(wú)須擔(dān)心與其它代碼創(chuàng)建的屬性產(chǎn)生沖突。
symbol鍵的設(shè)計(jì)初衷是避免初衷,因此JavaScript中最常見(jiàn)的對(duì)象檢查的特性會(huì)忽略symbol鍵。例如,for-in循環(huán)只會(huì)遍歷對(duì)象的字符串鍵,symbol鍵直接跳過(guò),Object.keys(obj)和Object.getOwnPropertyNames(obj)也是一樣。但是symbols也不完全是私有的:用新的API Object.getOwnPropertySymbols(obj)就可以列出對(duì)象的symbol鍵。另一個(gè)新的API,Reflect.ownKeys(obj),會(huì)同時(shí)返回字符串鍵和symbol鍵。(我們將在隨后的文章中講解Reflect(反射) API)。
慢慢地我們會(huì)發(fā)現(xiàn),越來(lái)越多的庫(kù)和框架將大量使用symbol,語(yǔ)言本身也會(huì)將symbol應(yīng)用于廣泛的用途。
但是,到底什么是symbol呢?
> typeof Symbol()
"symbol"
確切地說(shuō),symbol與其它類(lèi)型并不完全相像。
symbol被創(chuàng)建后就不可變更,你不能為它設(shè)置屬性(在嚴(yán)格模式下嘗試設(shè)置屬性會(huì)得到TypeError的錯(cuò)誤)。他們可以用作屬性名稱,這些性質(zhì)與字符串類(lèi)似。
另一方面,每一個(gè)symbol都獨(dú)一無(wú)二,不與其它symbol等同,即使二者有相同的描述也不相等;你可以輕松地創(chuàng)建一個(gè)新的symbol。這些性質(zhì)與對(duì)象類(lèi)似。
ES6中的symbol與Lisp和Ruby這些語(yǔ)言中更傳統(tǒng)的symbol類(lèi)似,但不像它們集成得那么緊密。在Lisp中,所有的標(biāo)識(shí)符都是symbol;在JS中,標(biāo)識(shí)符和大多數(shù)的屬性鍵仍然是字符串,symbol只是一個(gè)額外的選項(xiàng)。
關(guān)于symbol的忠告:symbol不能被自動(dòng)轉(zhuǎn)換為字符串,這和語(yǔ)言中的其它類(lèi)型不同。嘗試拼接symbol與字符串將得到TypeError錯(cuò)誤。
> var sym = Symbol("<3");
> "your symbol is " + sym
// TypeError: can't convert symbol to string
> `your symbol is ${sym}`
// TypeError: can't convert symbol to string
通過(guò)String(sym)或sym.toString()可以顯示地將symbol轉(zhuǎn)換為一個(gè)字符串,從而回避這個(gè)問(wèn)題。
獲取symbol的三種方法
有三種獲取symbol的方法。
- 調(diào)用Symbol()。正如我們上文中所討論的,這種方式每次調(diào)用都會(huì)返回一個(gè)新的唯一symbol。
- 調(diào)用Symbol.for(string)。這種方式會(huì)訪問(wèn)symbol注冊(cè)表,其中存儲(chǔ)了已經(jīng)存在的一系列symbol。這種方式與通過(guò)Symbol()定義的獨(dú)立symbol不同,symbol注冊(cè)表中的symbol是共享的。如果你連續(xù)三十次調(diào)用Symbol.for("cat"),每次都會(huì)返回相同的symbol。注冊(cè)表非常有用,在多個(gè)web頁(yè)面或同一個(gè)web頁(yè)面的多個(gè)模塊中經(jīng)常需要共享一個(gè)symbol。
- 使用標(biāo)準(zhǔn)定義的symbol,例如:Symbol.iterator。標(biāo)準(zhǔn)根據(jù)一些特殊用途定義了少許的幾個(gè)symbol。
如果你尚不確定symbol是否實(shí)用,最后這一章將向你展示symbol在實(shí)際應(yīng)用中發(fā)揮的巨大作用,非常有趣!
symbol在ES6規(guī)范中的應(yīng)用
在之前的文章《深入淺出ES6(二):迭代器和for-of循環(huán)》中,我們已經(jīng)領(lǐng)略了借助ES6 symbol的力量避免代碼沖突的方法,循環(huán)for (var item of myArray)首先調(diào)用myArray[Symbol.iterator](),當(dāng)時(shí)我提到這種寫(xiě)法是為了替代myArray.iterator(),擁有更好的向后兼容性。
現(xiàn)在我們知道symbol到底是什么了,自然很容易理解為什么我們要?jiǎng)?chuàng)造一個(gè)symbol以及它為我們帶來(lái)什么新特性。
ES6中還有其它幾處使用了symbol的地方。(這些特性在Firefox里尚未實(shí)現(xiàn)。)
- 使instanceof可擴(kuò)展。在ES6中,表達(dá)式object instanceof constructor被指定為構(gòu)造函數(shù)的一個(gè)方法:constructor[Symbol.hasInstance](object)。這意味著它是可擴(kuò)展的。
- 消除新特性和舊代碼之間的沖突。這一點(diǎn)非常復(fù)雜,但是我們發(fā)現(xiàn),添加某些ES6數(shù)組方法會(huì)破壞現(xiàn)有的Web網(wǎng)站。其它Web標(biāo)準(zhǔn)有相同的問(wèn)題:向?yàn)g覽器中添加新方法會(huì)破壞原有的網(wǎng)站。然而,破壞問(wèn)題主要由動(dòng)態(tài)作用域引起,所以ES6引入一個(gè)特殊的symbol——Symbol.unscopables,Web標(biāo)準(zhǔn)可以用這個(gè)symbol來(lái)阻止某些方法別加入到動(dòng)態(tài)作用域中。
- 支持新的字符串匹配類(lèi)型。在ES5中,str.match(myObject)會(huì)嘗試將myObject轉(zhuǎn)換為正則表達(dá)式對(duì)象(RegExp)。在ES6中,它會(huì)首先檢查myObject是否有一個(gè)myObject[Symbol.match](str)方法。現(xiàn)在的庫(kù)可以提供自定義的字符串解析類(lèi),所有支持RegExp對(duì)象的環(huán)境都可以正常運(yùn)行。
這些用例的應(yīng)用范圍都非常小,很難看到這些特性通過(guò)它們自身影響我們每日的代碼,長(zhǎng)期來(lái)看才能體現(xiàn)它們的價(jià)值。實(shí)際上,symbol是php和Python中的__doubleUnderscores在JavaScript語(yǔ)言環(huán)境中的改進(jìn)版。標(biāo)準(zhǔn)將借助symbol的力量在未來(lái)向語(yǔ)言中添加新的鉤子,同時(shí)無(wú)風(fēng)險(xiǎn)地將新特性添加到你已有的代碼中。
我何時(shí)可以使用ES6 symbol?
symbol在Firefox 36和Chrome 38中均已被實(shí)現(xiàn)。Firefox中的實(shí)現(xiàn)由我親自完成,所以如果你的symbol像鐃鈸(cymbals)一樣行為異常,請(qǐng)直接聯(lián)系我!
為了支持那些尚未支持原生ES6 symbol的瀏覽器,你可以使用一個(gè)polyfill,例如core.js。因?yàn)閟ymbol與其它類(lèi)型不盡相同,所以polyfill目前不是很完美。請(qǐng)閱讀注意事項(xiàng)。
集合
前段時(shí)間,官方名為“ECMA-262,第六版,ECMAScript 2015語(yǔ)言規(guī)范”的ES6規(guī)范終于結(jié)束了最后的征途,正式被認(rèn)可為新的ECMA標(biāo)準(zhǔn)。讓我們祝賀TC39等所有作出貢獻(xiàn)人們,ES6終于定稿了!
更好的消息是,下次更新不需要再等六年了。委員會(huì)現(xiàn)在努力要求,大約每12個(gè)月完成一個(gè)新的版本。第七版提議已經(jīng)開(kāi)始。
現(xiàn)在是時(shí)候慶祝慶祝了,讓我們來(lái)討論一些很久以來(lái)我一直希望在JS里看到的東西——當(dāng)然,它們以后仍然有改進(jìn)的余地。
共同發(fā)展中的難題
JS和其它編程語(yǔ)言有些特殊的差別,有時(shí),它們會(huì)以令人驚奇的方式影響到這門(mén)語(yǔ)言的發(fā)展。
ES6模塊就是個(gè)很好的例子。其它語(yǔ)言的模塊化系統(tǒng)中,Racket做得特別棒,Python也很好。那么,當(dāng)標(biāo)準(zhǔn)委員會(huì)決定在ES6中增加模塊時(shí),為什么他們不直接仿照一套已經(jīng)存在的系統(tǒng)呢?
因?yàn)镴S是不同的,因?yàn)樗跒g覽器里運(yùn)行。讀取和寫(xiě)入都可能花費(fèi)較長(zhǎng)時(shí)間,所以,JS需要一套支持異步加載代碼的模塊化系統(tǒng),同時(shí),也不能允許在文件夾中挨個(gè)搜索,照搬已有的系統(tǒng)并不能解決問(wèn)題。ES6的模塊化系統(tǒng)需要一些新技術(shù)。
討論這些問(wèn)題對(duì)最終設(shè)計(jì)的影響,會(huì)是個(gè)有趣的故事,不過(guò)我們今天要討論的并不是模塊。
這篇文章是關(guān)于ES6標(biāo)準(zhǔn)中所謂“鍵值集合”的:Set,Map,WeakSet和WeakMap。它們?cè)诖蠖鄶?shù)方面和其它語(yǔ)言中的哈希表一樣,不過(guò),正因?yàn)镴S是不同的,標(biāo)準(zhǔn)委員會(huì)在其中做了些有趣的權(quán)衡與調(diào)整。
為什么要集合?
熟悉JS一定會(huì)知道,我們已經(jīng)有了一種類(lèi)似哈希表的東西:對(duì)象(Object)。
一個(gè)普通的對(duì)象畢竟就只是一個(gè)開(kāi)放的鍵值對(duì)集合。你可以進(jìn)行獲取、設(shè)置、刪除、遍歷——任何一個(gè)哈希表支持的操作。所以我們到底為什么要增加新的特性?
好吧,大多數(shù)程序簡(jiǎn)單地用對(duì)象來(lái)存儲(chǔ)鍵值對(duì)就夠了,對(duì)它們而言,沒(méi)什么必要換用Map或Set。但是,直接這樣使用對(duì)象有一些廣為人知的問(wèn)題:
- 作為查詢表使用的對(duì)象,不能既支持方法又保證避免沖突。
- 因而,要么得用Object.create(null)而非直接寫(xiě){},要么得小心地避免把Object.prototype.toString之類(lèi)的內(nèi)置方法名作為鍵名來(lái)存儲(chǔ)數(shù)據(jù)。
- 對(duì)象的鍵名總是字符串(當(dāng)然,ES6 中也可以是Symbol)而不能是另一個(gè)對(duì)象。
- 沒(méi)有有效的獲知屬性個(gè)數(shù)的方法。
ES6中又出現(xiàn)了新問(wèn)題:純粹的對(duì)象不可遍歷,也就是,它們不能配合for-of循環(huán)或...操作符等語(yǔ)法。
嗯,確實(shí)很多程序里這些問(wèn)題都不重要,直接用純對(duì)象仍然是正確的選擇。Map和Set是為其它場(chǎng)合準(zhǔn)備的。
這些ES6中的集合本來(lái)就是為避免用戶數(shù)據(jù)與內(nèi)置方法沖突而設(shè)計(jì)的,所以它們不會(huì)把數(shù)據(jù)作為屬性暴露出來(lái)。也就是說(shuō),obj.key或obj[key]不能再用來(lái)訪問(wèn)數(shù)據(jù)了,取而代之的是map.get(key)。同時(shí),不像屬性,哈希表的鍵值不能通過(guò)原型鏈來(lái)繼承了。
好消息是,不像純粹的Object,Map和Set有自己的方法了,并且,更多標(biāo)準(zhǔn)或自定義的方法可以無(wú)需擔(dān)心沖突地加入。
Set
一個(gè)Set是一群值的集合。它是可變的,能夠增刪元素。現(xiàn)在,還沒(méi)說(shuō)到它和數(shù)組的區(qū)別,不過(guò)它們的區(qū)別就和相似點(diǎn)一樣多。
首先,和數(shù)組不同,一個(gè)Set不會(huì)包含相同元素。試圖再次加入一個(gè)已有元素不會(huì)產(chǎn)生任何效果。
這個(gè)例子里元素都是字符串,不過(guò)Set是可以包含JS中任何類(lèi)型的值的。同樣,重復(fù)加入已有元素不會(huì)產(chǎn)生效果。
其次,Set的數(shù)據(jù)存儲(chǔ)結(jié)構(gòu)專門(mén)為一種操作作了速度優(yōu)化:包含性檢測(cè)。
> // 檢查"zythum"是不是一個(gè)單詞
> arrayOfWords.indexOf("zythum") !== -1 // 慢
true
> setOfWords.has("zythum") // 快
true
Set不能提供的則是索引。
> arrayOfWords[15000]
"anapanapa"
> setOfWords[15000] // Set不支持索引
undefined
以下是Set支持的所有操作:
- new Set:創(chuàng)建一個(gè)新的、空的Set。
- new Set(iterable):從任何可遍歷數(shù)據(jù)中提取元素,構(gòu)造出一個(gè)新的集合。
- set.size:獲取集合的大小,即其中元素的個(gè)數(shù)。
- set.has(value):判定集合中是否含有指定元素,返回一個(gè)布爾值。
- set.add(value):添加元素。如果與已有重復(fù),則不產(chǎn)生效果。
- set.delete(value):刪除元素。如果并不存在,則不產(chǎn)生效果。.add()和.delete()都會(huì)返回集合自身,所以我們可以用鏈?zhǔn)秸Z(yǔ)法。
- set[Symbol.iterator]():返回一個(gè)新的遍歷整個(gè)集合的迭代器。一般這個(gè)方法不會(huì)被直接調(diào)用,因?yàn)閷?shí)際上就是它使集合能夠被遍歷,也就是說(shuō),我們可以直接寫(xiě)for (v of set) {...}等等。
- set.forEach(f):直接用代碼來(lái)解釋好了,它就像是for (let value of set) { f(value, value, set); }的簡(jiǎn)寫(xiě),類(lèi)似于數(shù)組的.forEach()方法。
- set.clear():清空集合。
- set.keys()、set.values()和set.entries()返回各種迭代器,它們是為了兼容Map而提供的,所以我們待會(huì)兒再來(lái)看。
在這些特性中,負(fù)責(zé)構(gòu)造集合的new Set(iterable)是唯一一個(gè)在整個(gè)數(shù)據(jù)結(jié)構(gòu)層面上操作的。你可以用它把數(shù)組轉(zhuǎn)化為集合,在一行代碼內(nèi)去重;也可以傳遞一個(gè)生成器,函數(shù)會(huì)逐個(gè)遍歷它,并把生成的值收錄為一個(gè)集合;也可以用來(lái)復(fù)制一個(gè)已有的集合。
上周我答應(yīng)過(guò)要給ES6中的新集合們挑挑刺,就從這里開(kāi)始吧。盡管Set已經(jīng)很不錯(cuò)了,還是有些被遺漏的方法,說(shuō)不定補(bǔ)充到將來(lái)某個(gè)標(biāo)準(zhǔn)里會(huì)挺不錯(cuò):
- 目前數(shù)組已經(jīng)有的一些輔助函數(shù),比如.map()、.filter()、.some()和.every()。
- 不改變?cè)档慕徊⒉僮鳎热鐂et1.union(set2)和set1.intersection(set2)。
- 批量操作,如set.addAll(iterable)、set.removeAll(iterable)和set.hasAll(iterable)。
好消息是,這些都可以用ES6已經(jīng)提供了的方法來(lái)實(shí)現(xiàn)。
Map
一個(gè)Map對(duì)象由若干鍵值對(duì)組成,支持:
- new Map:返回一個(gè)新的、空的Map。
- new Map(pairs):根據(jù)所含元素形如[key, value]的數(shù)組pairs來(lái)創(chuàng)建一個(gè)新的Map。這里提供的pairs可以是一個(gè)已有的Map 對(duì)象,可以是一個(gè)由二元數(shù)組組成的數(shù)組,也可以是逐個(gè)生成二元數(shù)組的一個(gè)生成器,等等。
- map.size:返回Map中項(xiàng)目的個(gè)數(shù)。
- map.has(key):測(cè)試一個(gè)鍵名是否存在,類(lèi)似key in obj。
- map.get(key):返回一個(gè)鍵名對(duì)應(yīng)的值,若鍵名不存在則返回undefined,類(lèi)似obj[key]。
- map.set(key, value):添加一對(duì)新的鍵值對(duì),如果鍵名已存在就覆蓋。
- map.delete(key):按鍵名刪除一項(xiàng),類(lèi)似delete obj[key]。
- map.clear():清空Map。
- map[Symbol.iterator]():返回遍歷所有項(xiàng)的迭代器,每項(xiàng)用一個(gè)鍵和值組成的二元數(shù)組表示。
- map.forEach(f) 類(lèi)似for (let [key, value] of map) { f(value, key, map); }。這里詭異的參數(shù)順序,和Set中一樣,是對(duì)應(yīng)著Array.prototype.forEach()。
- map.keys():返回遍歷所有鍵的迭代器。
- map.values():返回遍歷所有值的迭代器。
- map.entries():返回遍歷所有項(xiàng)的迭代器,就像map[Symbol.iterator]()。實(shí)際上,它們就是同一個(gè)方法,不同名字。
還有什么要抱怨的?以下是我覺(jué)得會(huì)有用而ES6還沒(méi)提供的特性:
- 鍵不存在時(shí)返回的默認(rèn)值,類(lèi)似 Python 中的collections.defaultdict。
- 一個(gè)可以叫Map.fromObject(obj)的輔助函數(shù),以便更方便地用構(gòu)造對(duì)象的語(yǔ)法來(lái)寫(xiě)出一個(gè)Map。
同樣,這些特性也是很容易加上的。
到這里,還記不記得,開(kāi)篇時(shí)我提到過(guò)運(yùn)行于瀏覽器對(duì)語(yǔ)言特性設(shè)計(jì)的特殊影響?現(xiàn)在要好好談一談這個(gè)問(wèn)題了。我已經(jīng)有了三個(gè)例子,以下是前兩個(gè)。
JS是不同的,第一部分:沒(méi)有哈希代碼的哈希表?
到目前為止,據(jù)我所知,ES6的集合類(lèi)完全不支持下述這種有用的特性。
比如說(shuō),我們有若干 URL 對(duì)象組成的Set:
var urls = new Set;
urls.add(new URL(location.href)); // 兩個(gè) URL 對(duì)象。
urls.add(new URL(location.href)); // 它們一樣么?
alert(urls.size); // 2
這兩個(gè) URL 應(yīng)該按相同處理,畢竟它們有完全一樣的屬性。但在JavaScript中,它們是各自獨(dú)立、互不相同的,并且,絕對(duì)沒(méi)有辦法來(lái)重載相等運(yùn)算符。
其它一些語(yǔ)言就支持這一特性。在Java, Python, Ruby中,每個(gè)類(lèi)都可以重載它的相等運(yùn)算符;Scheme的許多實(shí)現(xiàn)中,每個(gè)哈希表可以使用不同的相等關(guān)系。C++則兩者都支持。
但是,所有這些機(jī)制都需要編寫(xiě)者自行實(shí)現(xiàn)一個(gè)哈希函數(shù)并暴露出系統(tǒng)默認(rèn)的哈希函數(shù)。在JS中,因?yàn)椴坏貌豢紤]其它語(yǔ)言不必?fù)?dān)心的互用性和安全性,委員會(huì)選擇了不暴露——至少目前仍如此。
JS是不同的,第二部分:意料之外的可預(yù)測(cè)性
你多半覺(jué)得一臺(tái)計(jì)算機(jī)具有確定性行為是理所應(yīng)當(dāng)?shù)模?dāng)我告訴別人遍歷Map或Set的順序就是其中元素的插入順序時(shí),他們總是很驚奇。沒(méi)錯(cuò),它就是確定的。
我們已經(jīng)習(xí)慣了哈希表某些方面任性的行為,我們學(xué)會(huì)了接受它。不過(guò),總有一些足夠好的理由讓我們希望嘗試避免這種不確定性。2012年我寫(xiě)過(guò):
- 有證據(jù)表明,部分程序員一開(kāi)始會(huì)覺(jué)得遍歷順序的不確定性是令人驚奇又困惑的。1 2 3 4 5 6
- ECMAScript中沒(méi)有明確規(guī)定遍歷屬性的順序,但為了兼容互聯(lián)網(wǎng)現(xiàn)狀,幾乎所有主流實(shí)現(xiàn)都不得不將其定義為插入順序。因此,有人擔(dān)心,假如TC39不確立一個(gè)確定的遍歷順序,“互聯(lián)網(wǎng)社區(qū)也會(huì)在自行發(fā)展中替我們決定。” 7
- 自定義哈希表的遍歷順序會(huì)暴露一些哈希對(duì)象的代碼,繼而引發(fā)關(guān)于哈希函數(shù)實(shí)現(xiàn)的一些惱人的安全問(wèn)題。例如,暴露出的代碼絕不能獲知一個(gè)對(duì)象的地址。(向不受信任的ES代碼透露對(duì)象地址而對(duì)其自身隱藏,將是互聯(lián)網(wǎng)的一大安全漏洞。)
在2012年2月以上種種意見(jiàn)被提出時(shí),我是支持不確定遍歷序的。然后,我決定用實(shí)驗(yàn)證明,保存插入序?qū)⑦^(guò)度降低哈希表的效率。我寫(xiě)了一個(gè)C++的小型基準(zhǔn)測(cè)試,結(jié)果卻令我驚奇地恰恰相反。
這就是我們最終為JS設(shè)計(jì)了按插入序遍歷的哈希表的過(guò)程。
推薦使用弱集合的重要原因
上篇文章我們討論了一個(gè)JS動(dòng)畫(huà)庫(kù)相關(guān)的例子。我們?cè)囍獮槊總€(gè)DOM對(duì)象設(shè)置一個(gè)布爾值類(lèi)型的標(biāo)識(shí)屬性,就像這樣:
if (element.isMoving) {
smoothAnimations(element);
}
element.isMoving = true;
不幸的是,這樣給一個(gè)DOM對(duì)象增加屬性不是個(gè)好主意。原因我們上次已經(jīng)解釋過(guò)了。
上次的文章里,我們接著展示了用Symbol解決這個(gè)問(wèn)題的方法。但是,可以用集合來(lái)實(shí)現(xiàn)同樣的效果么?也許看上去會(huì)像這樣:
if (movingSet.has(element)) {
smoothAnimations(element);
}
movingSet.add(element);
這只有一個(gè)壞處。Map和Set都為內(nèi)部的每個(gè)鍵或值保持了強(qiáng)引用,也就是說(shuō),如果一個(gè)DOM元素被移除了,回收機(jī)制無(wú)法取回它占用的內(nèi)存,除非movingSet中也刪除了它。在最理想的情況下,庫(kù)在善后工作上對(duì)使用者都有復(fù)雜的要求,所以,這很可能引發(fā)內(nèi)存泄露。
ES6給了我們一個(gè)驚喜的解決方案:用WeakSet而非Set。和內(nèi)存泄露說(shuō)再見(jiàn)吧!
也 就是說(shuō),這個(gè)特定情景下的問(wèn)題可以用弱集合(weak collection)或Symbol兩種方法解決。哪個(gè)更好呢?不幸的是,完整地討論利弊取舍會(huì)把這篇文章拖得有些長(zhǎng)。簡(jiǎn)而言之,如果能在整個(gè)網(wǎng)頁(yè)的生 命周期內(nèi)使用同一個(gè)Symbol,那就沒(méi)什么問(wèn)題;如果不得不使用一堆臨時(shí)的Symbol,那就危險(xiǎn)了,是時(shí)候考慮WeakMap來(lái)避免內(nèi)存泄露了。
WeakMap和WeakSet
WeakMap和WeakSet被設(shè)計(jì)來(lái)完成與Map、Set幾乎一樣的行為,除了以下一些限制:
- WeakMap只支持new、has、get、set 和delete。
- WeakSet只支持new、has、add和delete。
- WeakSet的值和WeakMap的鍵必須是對(duì)象。
還要注意,這兩種弱集合都不可迭代,除非專門(mén)查詢或給出你感興趣的鍵,否則不能獲得一個(gè)弱集合中的項(xiàng)。
這些小心設(shè)計(jì)的限制讓垃圾回收機(jī)制能回收仍在使用中的弱集合里的無(wú)效對(duì)象。這效果類(lèi)似于弱引用或弱鍵字典,但ES6的弱集合可以在不暴露腳本中正在垃圾回收的前提下得到垃圾回收的效益。
JS是不同的,第三部分:隱藏垃圾回收的不確定性
弱集合實(shí)際上是用 ephemeron 表實(shí)現(xiàn)的。
簡(jiǎn)單說(shuō),一個(gè)WeakSet并不對(duì)其中對(duì)象保持強(qiáng)引用。當(dāng)WeakSet中的一個(gè)對(duì)象被回收時(shí),它會(huì)簡(jiǎn)單地被從WeakSet中移除。WeakMap也類(lèi)似地不為它的鍵保持強(qiáng)引用。如果一個(gè)鍵仍被使用,相應(yīng)的值也就仍被使用。
為什么要接受這些限制呢?為什么不直接在JS中引入弱引用呢?
再 次地,這是因?yàn)闃?biāo)準(zhǔn)委員會(huì)很不愿意向腳本暴露未定義行為。孱弱的跨瀏覽器兼容性是互聯(lián)網(wǎng)發(fā)展的痛苦之源。弱引用暴露了底層垃圾回收的實(shí)現(xiàn)細(xì)節(jié)——這正是與 平臺(tái)相關(guān)的一個(gè)未定義行為。應(yīng)用當(dāng)然不應(yīng)該依賴平臺(tái)相關(guān)的細(xì)節(jié),但弱引用使我們難于精確了解自己對(duì)測(cè)試使用的瀏覽器的依賴程度。這是件很不講道理的事情。
相比之下,ES6的弱集合只包含了一套有限的特性,但它們相當(dāng)牢靠。一個(gè)鍵或值被回收從不會(huì)被觀測(cè)到,所以應(yīng)用將不會(huì)依賴于其行為,即使只是緣于意外。
這是針對(duì)互聯(lián)網(wǎng)的特殊考量引發(fā)了一個(gè)驚人的設(shè)計(jì)、進(jìn)而使JS成為一門(mén)更好語(yǔ)言的一個(gè)例子。
什么時(shí)候可以用上這些集合呢?
總計(jì)四種集合類(lèi)在Firefox、Chrome、Microsoft Edge、Safari中都已實(shí)現(xiàn),要支持舊瀏覽器則需要 ES6 - Collections 之類(lèi)來(lái)補(bǔ)全。
Firefox中的WeakMap 最初由 Andreas Gal 實(shí)現(xiàn),他后來(lái)當(dāng)了一段時(shí)間Mozilla的CTO。Tom Schuster實(shí)現(xiàn)了WeakSet,我實(shí)現(xiàn)了Map和Set。感謝Tooru Fujisawa貢獻(xiàn)的幾個(gè)相關(guān)補(bǔ)丁。
學(xué)習(xí)Babel和Broccoli,馬上就用ES6
自ES6正式發(fā)布,人們已經(jīng)開(kāi)始討論ES7:未來(lái)版本會(huì)保留哪些特性,新標(biāo)準(zhǔn)可能提供什么樣的新特性。作為Web開(kāi)發(fā)者,我們想知道如何發(fā)揮這一切的巨大能量。在深入淺出ES6系列之前的文章中,我們不斷鼓勵(lì)你開(kāi)始在編碼中加入ES6新特性,輔以一些有趣的工具,你完全可以從現(xiàn)在開(kāi)始使用ES6:
如果你想在Web端使用這種新語(yǔ)法,你可以通過(guò)Babel或Google的Traceur將你的ES6代碼轉(zhuǎn)譯為Web友好的ES5代碼。
現(xiàn)在,我們將向你分步展示如何做到的這一切。上面提及的工具被稱為轉(zhuǎn)譯器,你可以將它理解為源代碼到源代碼的編譯器——一個(gè)在可比較的抽象層上操作不同編程語(yǔ)言相互轉(zhuǎn)換的編譯器。轉(zhuǎn)譯器允許我們用ES6編寫(xiě)代碼,同時(shí)保證這些代碼能在每一個(gè)瀏覽器中執(zhí)行。
轉(zhuǎn)譯技術(shù)拯救了我們
轉(zhuǎn)譯器使用起來(lái)非常簡(jiǎn)單,只需兩步即可描述它所做的事情:
1,用ES6的語(yǔ)法編寫(xiě)代碼。
let q = 99;
let myVariable = `${q} bottles of beer on the wall, ${q} bottles of beer.`;
2,用上面那段代碼作為轉(zhuǎn)譯器的輸入,經(jīng)過(guò)處理后得到以下這段輸出:
"use strict";
var q = 99;
var myVariable = "" + q + " bottles of beer on the wall, " + q + " bottles of beer."
這正是我們熟知的老式JavaScript,這段代碼可以在任意瀏覽器中運(yùn)行。
轉(zhuǎn)譯器內(nèi)部從輸入到輸出的邏輯高度復(fù)雜,完全超出本篇文章的講解范圍。正如我們無(wú)須知道所有的內(nèi)部引擎結(jié)構(gòu)就可以駕駛一輛汽車(chē),現(xiàn)在,我們同樣可以將轉(zhuǎn)譯器視為一個(gè)能夠處理我們代碼的黑盒。
實(shí)際體驗(yàn)Babel
你可以通過(guò)幾種不同的方法在項(xiàng)目中使用Babel,有一個(gè)命令行工具,在這個(gè)工具中可以使用如下形式的指令:
babel script.js --out-file script-compiled.js
Babel也提供支持在瀏覽器中使用的版本。你可以將Babel作為一個(gè)普通的庫(kù)引入,然后將你的ES6代碼放置在類(lèi)型為text/babel的script標(biāo)簽中。
<script src="node_modules/babel-core/browser.js"></script>
<script type="text/babel">
// 你的ES6代碼
</script>
隨著代碼庫(kù)爆炸式增長(zhǎng),你開(kāi)始將所有代碼劃分為多個(gè)文件和文件夾,但是這些方法并不能隨之?dāng)U展。到那時(shí),你將需要一個(gè)構(gòu)建工具以及一種將Babel與構(gòu)建管道整合在一起的方法。
在接下來(lái)的章節(jié)中,我們將要把Babel整合到構(gòu)建工具Broccoli.js中,我們將在兩個(gè)示例中編寫(xiě)并執(zhí)行第一行ES6代碼。如果你的代碼無(wú)法正常運(yùn)行,可以在這里(broccoli-babel-examples)查看完整的源代碼。在這個(gè)倉(cāng)庫(kù)中你可以找到三個(gè)示例項(xiàng)目:
- es6-fruits
- es6-website
- es6-modules
每一個(gè)項(xiàng)目都構(gòu)建于前一個(gè)示例的基礎(chǔ)之上,我們會(huì)從最小的項(xiàng)目開(kāi)始,逐步得出一個(gè)一般的解決方案,為日后每一個(gè)雄心壯志的項(xiàng)目打下良好的開(kāi)端。這篇文章只包含前兩個(gè)示例,閱讀文章后,你完全可以自行閱讀第三個(gè)示例中的代碼并加以理解。
如果你在想——我坐等瀏覽器支持這些新特性就好了啦——那么你一定會(huì)落后的!實(shí)現(xiàn)所有功能要花費(fèi)很長(zhǎng)時(shí)間,況且現(xiàn)在有成熟的轉(zhuǎn)譯器,而且 ECMAScript加快了發(fā)布新版本的周期(每年一版),我們將會(huì)看到新標(biāo)準(zhǔn)比統(tǒng)一的瀏覽器平臺(tái)更新得更頻繁。所以趕快加入我們,一起發(fā)揮新特性的巨大威力吧!
我們的首個(gè)Broccoli與Babel項(xiàng)目
Broccoli是一個(gè)用來(lái)快速構(gòu)建項(xiàng)目的工具,你可以用它對(duì)文件進(jìn)行混淆與壓縮,還可以通過(guò)眾多的Broccoli插件實(shí)現(xiàn)許多其它功能。它幫助我們處理文件和目錄,每當(dāng)項(xiàng)目變更時(shí)自動(dòng)執(zhí)行指令,很大程度上減輕了我們的負(fù)擔(dān)。你不妨將它視為:
類(lèi)似Rails的asset管道,但是Broccoli運(yùn)行在Node上且可以對(duì)接任意后端。
配置項(xiàng)目
NODE
你可能已經(jīng)猜到了,你需要安裝Node 0.11或更高版本。
如果你使用unix系統(tǒng),不要從包管理器(apt、yum等)中安裝,這樣可以避免在安裝過(guò)程中使用root權(quán)限,最好使用當(dāng)前的用戶權(quán)限,通過(guò)上面的鏈接手動(dòng)安裝。在文章《不要sudo npm》中可以了解為什么不推薦使用root權(quán)限,文章中也給出了其它安裝方案。
BROCCOLI
首先,我們要配置好Broccoli項(xiàng)目:
mkdir es6-fruits
cd es6-fruits
npm init
# 創(chuàng)建一個(gè)名為Brocfile.js的空文件
touch Brocfile.js
現(xiàn)在我們安裝broccoli和broccoli-cli
# 安裝broccoli庫(kù)
npm install --save-dev broccoli
# 命令行工具
npm install -g broccoli-cli
編寫(xiě)一些ES6代碼
創(chuàng)建src文件夾,在里面置入fruits.js文件。
mkdir src
vim src/fruits.js
用ES6語(yǔ)法在新文件中寫(xiě)一小段腳本。
let fruits = [
{id: 100, name: '草莓'},
{id: 101, name: '柚子'},
{id: 102, name: '李子'}
];
for (let fruit of fruits) {
let message = `ID: ${fruit.id} Name: ${fruit.name}`;
console.log(message);
}
console.log(`List total: ${fruits.length}`);
上面的代碼示例使用了三個(gè)ES6特性:
- 用let進(jìn)行局部作用域聲明(在稍后的文章中討論)
- for-of循環(huán)
- 模板字符串
保存文件,嘗試執(zhí)行腳本。
node src/fruits.js
目前這段代碼不能正常運(yùn)行,但是我們將會(huì)讓它運(yùn)行在Node與任何瀏覽器中。
let fruits = [
^^^^^^
SyntaxError: Unexpected identifier
轉(zhuǎn)譯時(shí)刻
現(xiàn)在,我們用Broccoli加載代碼,然后用Babel處理它。編輯Brocfile.js文件并加入以下這段代碼:
// 引入babel插件
var babel = require('broccoli-babel-transpiler');
// 獲取源代碼,執(zhí)行轉(zhuǎn)譯指令(僅需1步)
fruits = babel('src'); // src/*.js
module.exports = fruits;
注意我們引入了包裹在Babel庫(kù)中的Broccoli插件broccoli-babel-transpiler,所以我們一定要安裝它:
npm install --save-dev broccoli-babel-transpiler
現(xiàn)在我們可以構(gòu)建項(xiàng)目并執(zhí)行腳本了:
broccoli build dist # 編譯
node dist/fruits.js # 執(zhí)行ES5
輸出結(jié)果看起來(lái)應(yīng)當(dāng)是這樣的:
ID: 100 Name: 草莓
ID: 101 Name: 柚子
ID: 102 Name: 李子
List total: 3
那很簡(jiǎn)單!你可以打開(kāi)dist/fruits.js查看轉(zhuǎn)譯后代碼。Babel轉(zhuǎn)譯器的一個(gè)優(yōu)秀特性是它能夠生產(chǎn)可讀的代碼。
為網(wǎng)站編寫(xiě)ES6代碼
在第二個(gè)示例中,我們將做進(jìn)一步提升。首先,退出es6-fruits文件夾,然后使用上述配置項(xiàng)目一章中列出的步驟創(chuàng)建新目錄es6-website。
在src文件夾中創(chuàng)建三個(gè)文件:src/index.html
<!DOCTYPE html>
<html>
<head>
<title>馬上使用ES6</title>
</head>
<style>
body {
border: 2px solid #9a9a9a;
border-radius: 10px;
padding: 6px;
font-family: monospace;
text-align: center;
}
.color {
padding: 1rem;
color: #fff;
}
</style>
<body>
<h1>馬上使用ES6</h1>
<div id="info"></div>
<hr>
<div id="content"></div>
<script src="//code.jquery.com/jquery-2.1.4.min.js"></script>
<script src="js/my-app.js"></script>
</body>
</html>src/print-info.js
function printInfo() {
$('#info')
.append('<p>用Broccoli和Babel構(gòu)建的' +
'最小網(wǎng)站示例</p>');
}
$(printInfo);src/print-colors.js
// ES6生成器
function* hexRange(start, stop, step) {
for (var i = start; i < stop; i += step) {
yield i;
}
}
function printColors() {
var content$ = $('#content');
// 人為的示例
for ( var hex of hexRange(900, 999, 10) ) {
var newDiv = $('<div>')
.attr('class', 'color')
.css({ 'background-color': `#${hex}` })
.append(`hex code: #${hex}`);
content$.append(newDiv);
}
}
$(printColors);
你可能已經(jīng)注意到function* hexRange,是的,那是ES6的生成器。這個(gè)特性目前尚未被所有瀏覽器支持。為了能夠使用這個(gè)特性,我們需要一個(gè)polyfill,Babel中已經(jīng)支持,我們很快將投入使用。
下一步是合并所有JS文件然后在網(wǎng)站中使用。最難的部分是編寫(xiě)B(tài)rocfile文件,這一次我們要安裝4個(gè)插件:
npm install --save-dev broccoli-babel-transpiler
npm install --save-dev broccoli-funnel
npm install --save-dev broccoli-concat
npm install --save-dev broccoli-merge-trees
把它們投入使用:
// Babel轉(zhuǎn)譯器
var babel = require('broccoli-babel-transpiler');
// 過(guò)濾樹(shù)(文件的子集)
var funnel = require('broccoli-funnel');
// 連結(jié)樹(shù)
var concat = require('broccoli-concat');
// 合并樹(shù)
var mergeTrees = require('broccoli-merge-trees');
// 轉(zhuǎn)譯源文件
var appJs = babel('src');
// 獲取Babel庫(kù)提供的polyfill文件
var babelPath = require.resolve('broccoli-babel-transpiler');
babelPath = babelPath.replace(//index.js$/, '');
babelPath += '/node_modules/babel-core';
var browserPolyfill = funnel(babelPath, {
files: ['browser-polyfill.js']
});
// 給轉(zhuǎn)譯后的文件樹(shù)添加Babel polyfill
appJs = mergeTrees([browserPolyfill, appJs]);
// 將所有JS文件連結(jié)為一個(gè)單獨(dú)文件
appJs = concat(appJs, {
// 我們指定一個(gè)連結(jié)順序
inputFiles: ['browser-polyfill.js', '**/*.js'],
outputFile: '/js/my-app.js'
});
// 獲取入口文件
var index = funnel('src', {files: ['index.html']});
// 獲取所有的樹(shù)
// 并導(dǎo)出最終單一的樹(shù)
module.exports = mergeTrees([index, appJs]);
現(xiàn)在開(kāi)始構(gòu)建并執(zhí)行我們的代碼。
broccoli build dist
這次你在dist文件夾中應(yīng)該看到以下結(jié)構(gòu):
$> tree dist/
dist/
├── index.html
└── js
└── my-app.js
那是一個(gè)靜態(tài)網(wǎng)站,你可以用任意服務(wù)器伺服來(lái)驗(yàn)證那段代碼正常運(yùn)行。舉個(gè)例子:
cd dist/
python -m SimpleHTTPServer
# 訪問(wèn)http://localhost:8000/
你應(yīng)該可以看到:
Babel和Broccoli組合還有更多樂(lè)趣
上述第二個(gè)示例給出了一個(gè)通過(guò)Babel實(shí)現(xiàn)功能的思路,它可能足夠你用上一陣子了。如果你想要更多有關(guān)ES6、Babel和Broccoli的內(nèi)容,可以查看broccoli-babel-boilerplate,這個(gè)倉(cāng)庫(kù)中的代碼可以提供Broccoli+Babel項(xiàng)目的配置,而且高出至少兩個(gè)層次。這個(gè)樣板可以文件處理模塊、模塊導(dǎo)入以及單元測(cè)試。
通過(guò)這些配置,你可以在示例es6-modules中親自實(shí)踐。Brocfile魔力無(wú)窮,與我們之前實(shí)現(xiàn)的非常類(lèi)似。
正如你看到的,Babel和Broccoli對(duì)于在Web網(wǎng)站中應(yīng)用ES6新特性非常實(shí)用。
代理 Proxies
請(qǐng)看這樣一段代碼:
var obj = new Proxy({}, {
get: function (target, key, receiver) {
console.log(`getting ${key}!`);
return Reflect.get(target, key, receiver);
},
set: function (target, key, value, receiver) {
console.log(`setting ${key}!`);
return Reflect.set(target, key, value, receiver);
}
});
代碼乍一看有些復(fù)雜,使用了一些陌生的特性,稍后我會(huì)詳細(xì)講解每一部分。現(xiàn)在,一起來(lái)看一下我們創(chuàng)建的對(duì)象:
> obj.count = 1;
setting count!
> ++obj.count;
getting count!
setting count!
2
顯示結(jié)果可能與我們的理解不太一樣,為什么會(huì)輸出“setting count”和“getting count”?其實(shí),我們攔截了這個(gè)對(duì)象的屬性訪問(wèn)方法,然后將“.”運(yùn)算符重載了。
它是如何做到的?
計(jì)算領(lǐng)域最好的的技巧是虛擬化,這種技術(shù)一般用來(lái)實(shí)現(xiàn)驚人的功能。它的工作機(jī)制如下:
- 隨便選一張照片。圖片來(lái)源:Martin Nikolaj Bech
- 在圖片中圍繞某物勾勒出一個(gè)輪廓。
- 現(xiàn)在替換掉輪廓中的內(nèi)容,或者替換掉輪廓外的內(nèi)容,但是始終要遵循向后兼容的規(guī)則,替換前后的圖片要盡可能相似,不能讓輪廓兩側(cè)的圖像過(guò)于突兀。圖片來(lái)源:Beverley Goodwin
你可能在《楚門(mén)的世界》和《黑客帝國(guó)》這類(lèi)經(jīng)典的計(jì)算機(jī)科學(xué)電影中見(jiàn)到過(guò)類(lèi)似的hack方法,將世界劃分為兩個(gè)部分,主人公生活在內(nèi)部世界,外部世界被精心編造的常態(tài)幻覺(jué)所替換。
為了滿足向后兼容的規(guī)則,你需要巧妙地設(shè)計(jì)填補(bǔ)進(jìn)去的圖片,但是真正的技巧是正確地勾勒輪廓。
我所謂的輪廓是指一個(gè)API邊界或接口,接口可以詳細(xì)說(shuō)明兩段代碼的交互方式以及交互雙方對(duì)另一半的需求。所以如果一旦在系統(tǒng)中設(shè)計(jì)好了接口,輪廓自然就清晰了,這樣就可以任意替換接口兩側(cè)的內(nèi)容而不影響二者的交互過(guò)程。
如果沒(méi)有現(xiàn)成的接口,就需要施展你的創(chuàng)意才華來(lái)創(chuàng)造新接口,有史以來(lái)最酷的軟件hack總是會(huì)勾勒一些之前從未有過(guò)的API邊界,然后通過(guò)大量的工程化實(shí)踐將接口引入到現(xiàn)有的體系中去。
虛擬內(nèi)存、硬件虛擬化、Docker、Valgrind、rr等不同抽象程度的項(xiàng)目都會(huì)基于現(xiàn)有的系統(tǒng)推動(dòng)開(kāi)發(fā)一些令人意想不到的新接口。在某些情況下,需要花費(fèi)數(shù)年的時(shí)間、新的操作系統(tǒng)特性甚至是新的硬件來(lái)使新的邊界良好運(yùn)轉(zhuǎn)。
最棒的虛擬化hack會(huì)帶來(lái)對(duì)需要虛擬的東西的新的理解。想要編寫(xiě)一個(gè)API,你需要充分理解你所面向的對(duì)象,一旦你理解透徹,就能實(shí)現(xiàn)出令人驚異的成果。
而ES6則為JavaScript中最基本的概念“對(duì)象(object)”引入了虛擬化支持。
所以,對(duì)象到底是什么?
噢,我是說(shuō)真的,請(qǐng)花費(fèi)一點(diǎn)時(shí)間仔細(xì)想想這個(gè)問(wèn)題的答案。當(dāng)你清楚自己知道對(duì)象是什么的的時(shí)候再向下滾動(dòng)。
這個(gè)問(wèn)題于我而言太難了!我從未聽(tīng)到過(guò)一個(gè)非常滿意的定義。
這會(huì)讓你感到驚訝么?定義基礎(chǔ)概念向來(lái)很困難——抽空看看歐幾里得在《幾何原本》中的前幾個(gè)定義你就知道了。ECMAScript語(yǔ)言規(guī)范很棒,可是卻將對(duì)象定義為“type對(duì)象的成員”,這種定義真的對(duì)我們沒(méi)什么幫助。
后來(lái),規(guī)范中又添加了一個(gè)定義:“對(duì)象是屬性的集合”。這句話沒(méi)錯(cuò),目前來(lái)說(shuō)可以這樣定義,我們稍后繼續(xù)討論。
我之前說(shuō)過(guò),想要編寫(xiě)一個(gè)API,你需要充分理解你所面向的對(duì)象。所以在某種程度上,我也算對(duì)本文做出一個(gè)承諾,我們會(huì)一起深入理解對(duì)象的細(xì)節(jié),然后一起實(shí)現(xiàn)酷炫的功能。
那么我們就跟隨ECMAScript標(biāo)準(zhǔn)委員會(huì)的腳步,為JavaScript對(duì)象定義一個(gè)API,一個(gè)接口。問(wèn)題是我們需要什么方法?對(duì)象又可以做什么呢?
這個(gè)問(wèn)題的答案一定程度上取決于對(duì)象的類(lèi)型:DOM元素對(duì)象可以做一部分事情,音頻節(jié)點(diǎn)對(duì)象又可以做另外一部分事情,但是所有對(duì)象都會(huì)共享一些基礎(chǔ)功能:
- 對(duì)象都有屬性。你可以get、set或刪除它們或做更多操作。
- 對(duì)象都有原型。這也是JS中繼承特性的實(shí)現(xiàn)方式。
- 有一些對(duì)象是可以被調(diào)用的函數(shù)或構(gòu)造函數(shù)。
幾乎所有處理對(duì)象的JS程序都是使用屬性、原型和函數(shù)來(lái)完成的。甚至元素或聲音節(jié)點(diǎn)對(duì)象的特殊行為也是通過(guò)調(diào)用繼承自函數(shù)屬性的方法來(lái)進(jìn)行訪問(wèn)。
所以ECMAScript標(biāo)準(zhǔn)委員會(huì)定義了一個(gè)由14種內(nèi)部方法組成的集合,亦即一個(gè)適用于所有對(duì)象的通用接口,屬性、原型和函數(shù)這三種基礎(chǔ)功能自然成為它們關(guān)注的核心。
我們可以在ES6標(biāo)準(zhǔn)列表5和6中找到全部的14種方法,我只會(huì)在這里講解其中一部分。雙方括號(hào)[[ ]]代表內(nèi)部方法,在一般的JS代碼中不可見(jiàn),你可以調(diào)用、刪除或覆寫(xiě)普通方法,但是無(wú)法操作內(nèi)部方法。
- obj.[[Get]](key, receiver) – 獲取屬性值。當(dāng)JS代碼執(zhí)行以下方法時(shí)被調(diào)用:obj.prop或obj[key]。obj是當(dāng)前被搜索的對(duì)象,receiver是我們首先開(kāi)始搜索這個(gè)屬性的對(duì)象。有時(shí)我們必須要搜索幾個(gè)對(duì)象,obj可能是一個(gè)在receiver原型鏈上的對(duì)象。
- obj.[[Set]](key, value, receiver) – 為對(duì)象的屬性賦值。當(dāng)JS代碼執(zhí)行以下方法時(shí)被調(diào)用:obj.prop = value或obj[key] = value。執(zhí)行類(lèi)似obj.prop += 2這樣的賦值語(yǔ)句時(shí),首先調(diào)用[[Get]]方法,然后調(diào)用[[Set]]方法。對(duì)于++和--操作符來(lái)說(shuō)亦是如此。
- obj.[HasProperty] – 檢測(cè)對(duì)象中是否存在某屬性。當(dāng)JS代碼執(zhí)行以下方法時(shí)被調(diào)用:key in obj。
- obj.[Enumerate] – 列舉對(duì)象的可枚舉屬性。當(dāng)JS代碼執(zhí)行以下方法時(shí)被調(diào)用:for (key in obj) …這個(gè)內(nèi)部方法會(huì)返回一個(gè)可迭代對(duì)象,for-in循環(huán)可通過(guò)這個(gè)方法得到對(duì)象屬性的名稱。
- obj.[GetPrototypeOf] – 返回對(duì)象的原型。當(dāng)JS代碼執(zhí)行以下方法時(shí)被調(diào)用:obj.[__proto__]或Object.getPrototypeOf(obj)。
- functionObj.[[Call]](thisValue, arguments) – 調(diào)用一個(gè)函數(shù)。當(dāng)JS代碼執(zhí)行以下方法時(shí)被調(diào)用:functionObj()或x.method()。可選的。不是每一個(gè)對(duì)象都是函數(shù)。
- constructorObj.[[Construct]](arguments, newTarget) – 調(diào)用一個(gè)構(gòu)造函數(shù)。當(dāng)JS代碼執(zhí)行以下方法時(shí)被調(diào)用:舉個(gè)例子,new Date(2890, 6, 2)。可選的。不是每一個(gè)對(duì)象都是構(gòu)造函數(shù)。參數(shù)newTarget在子類(lèi)中起一定作用,我們將在未來(lái)的文章中詳細(xì)講解。
可能你也可以猜到其它七個(gè)內(nèi)部方法。
在整個(gè)ES6標(biāo)準(zhǔn)中,只要有可能,任何語(yǔ)法或?qū)ο笙嚓P(guān)的內(nèi)建函數(shù)都是基于這14種內(nèi)部方法構(gòu)建的。ES6在對(duì)象的中樞系統(tǒng)周?chē)鷦澐至艘粋€(gè)清晰的界限,你可以借助代理特性用任意JS代碼替換標(biāo)準(zhǔn)中樞系統(tǒng)的內(nèi)部方法。
既然我們馬上要開(kāi)始討論覆寫(xiě)內(nèi)部方法的相關(guān)問(wèn)題,請(qǐng)記住,我們要討論的是諸如obj.prop的核心語(yǔ)法、諸如Object.keys()的內(nèi)建函數(shù)等的行為。
代理 Proxy
ES6規(guī)范定義了一個(gè)全新的全局構(gòu)造函數(shù):代理(Proxy)。它可以接受兩個(gè)參數(shù):目標(biāo)對(duì)象(target)與句柄對(duì)象(handler)。請(qǐng)看一個(gè)簡(jiǎn)單的示例:
var target = {}, handler = {};
var proxy = new Proxy(target, handler);
我們先來(lái)探討代理和目標(biāo)對(duì)象之間的關(guān)系,然后再研究句柄對(duì)象的功用。
代理的行為很簡(jiǎn)單:將代理的所有內(nèi)部方法轉(zhuǎn)發(fā)至目標(biāo)。簡(jiǎn)單來(lái)說(shuō),如果調(diào)用proxy.[[Enumerate]](),就會(huì)返回target.[[Enumerate]]()。
現(xiàn)在,讓我們嘗試執(zhí)行一條能夠觸發(fā)調(diào)用proxy.[[Set]]()方法的語(yǔ)句。
proxy.color = "pink";
好的,剛剛都發(fā)生了什么?proxy.[[Set]]()應(yīng)該調(diào)用target.[[Set]]()方法,然后在目標(biāo)上創(chuàng)建一個(gè)新的屬性。實(shí)際的結(jié)果如何?
> target.color
"pink"
是的,它做到了!對(duì)于所有其它內(nèi)部方法而言同樣可以做到。新創(chuàng)建的代理會(huì)盡可能與目標(biāo)的行為一致。
當(dāng)然,它們也不完全相同,你會(huì)發(fā)現(xiàn)proxy !== target。有時(shí)也有目標(biāo)能夠通過(guò)類(lèi)型檢測(cè)而代理無(wú)法通過(guò)的情況發(fā)生,舉個(gè)例子,如果代理的目標(biāo)是一個(gè)DOM元素,相應(yīng)的代理就不是,此時(shí)類(lèi)似document.body.appendChild(proxy)的操作會(huì)觸發(fā)類(lèi)型錯(cuò)誤(TypeError)。
代理句柄
現(xiàn)在我們繼續(xù)來(lái)討論一個(gè)讓代理充滿魔力的功能:句柄對(duì)象。
句柄對(duì)象的方法可以覆寫(xiě)任意代理的內(nèi)部方法。
舉個(gè)例子,你可以定義一個(gè)handler.set()方法來(lái)攔截所有給對(duì)象屬性賦值的行為:
var target = {};
var handler = {
set: function (target, key, value, receiver) {
throw new Error("請(qǐng)不要為這個(gè)對(duì)象設(shè)置屬性。");
}
};
var proxy = new Proxy(target, handler);
> proxy.name = "angelina";
Error: 請(qǐng)不要為這個(gè)對(duì)象設(shè)置屬性。
句柄方法的完整列表可以在MDN有關(guān)代理的頁(yè)面上找到,一共有14種方法,與ES6中定義的14中內(nèi)部方法一致。
所有句柄方法都是可選的,沒(méi)被句柄攔截的內(nèi)部方法會(huì)直接指向目標(biāo),與我們之前看到的別無(wú)二致。
小試牛刀(一):“不可能實(shí)現(xiàn)的”自動(dòng)填充對(duì)象
到目前為止,我們對(duì)于代理的了解程度足夠嘗試去做一些奇怪的事情,實(shí)現(xiàn)一些不借助代理根本無(wú)法實(shí)現(xiàn)的功能。
我們的第一個(gè)實(shí)踐,創(chuàng)建一個(gè)Tree()函數(shù)來(lái)實(shí)現(xiàn)以下特性:
> var tree = Tree();
> tree
{ }
> tree.branch1.branch2.twig = "green";
> tree
{ branch1: { branch2: { twig: "green" } } }
> tree.branch1.branch3.twig = "yellow";
{ branch1: { branch2: { twig: "green" },
branch3: { twig: "yellow" }}}
請(qǐng)注意,當(dāng)我們需要時(shí),所有中間對(duì)象branch1、branch2和branch3都可以自動(dòng)創(chuàng)建。這固然很方便,但是如何實(shí)現(xiàn)呢?
在這之前,沒(méi)有可以實(shí)現(xiàn)這種特性的方法,但是通過(guò)代理,我們只用寥寥幾行就可以輕松實(shí)現(xiàn),然后只需要接入tree.[[Get]]()就可以。如果你喜歡挑戰(zhàn),在繼續(xù)閱讀前可以嘗試自己實(shí)現(xiàn)。
這里是我的解決方案:
function Tree() {
return new Proxy({}, handler);
}
var handler = {
get: function (target, key, receiver) {
if (!(key in target)) {
target[key] = Tree(); // 自動(dòng)創(chuàng)建一個(gè)子樹(shù)
}
return Reflect.get(target, key, receiver);
}
};
注意最后的Reflect.get()調(diào)用,在代理句柄方法中有一個(gè)極其常見(jiàn)的需求:只執(zhí)行委托給目標(biāo)的默認(rèn)行為。所以ES6定義了一個(gè)新的反射(Reflect)對(duì)象,在其上有14種方法,你可以用它來(lái)實(shí)現(xiàn)這一需求。
小試牛刀(二):只讀視圖
我想我可能傳達(dá)給你們一個(gè)錯(cuò)誤的印象,也就是代理易于使用。接下來(lái)的這個(gè)示例可能會(huì)讓你稍感困頓。
這一次我們的賦值語(yǔ)句更復(fù)雜:我們需要實(shí)現(xiàn)一個(gè)函數(shù),readOnlyView(object),它可以接受任何對(duì)象作為參數(shù),并返回一個(gè)與此對(duì)象行為一致的代理,該代理不可被變更,就像這樣:
> var newMath = readOnlyView(Math);
> newMath.min(54, 40);
40
> newMath.max = Math.min;
Error: can't modify read-only view
> delete newMath.sin;
Error: can't modify read-only view
我們?nèi)绾螌?shí)現(xiàn)這樣的功能?
即使我們不會(huì)阻斷內(nèi)部方法的行為,但仍然要對(duì)其進(jìn)行干預(yù),所以第一步是攔截可能修改目標(biāo)對(duì)象的五種內(nèi)部方法。
function NOPE() {
throw new Error("can't modify read-only view");
}
var handler = {
// 覆寫(xiě)所有五種可變方法。
set: NOPE,
defineProperty: NOPE,
deleteProperty: NOPE,
dd preventExtensions: NOPE,
setPrototypeOf: NOPE
};
function readOnlyView(target) {
return new Proxy(target, handler);
}
這段代碼可以正常運(yùn)行,它借助只讀視圖阻止了賦值、屬性定義等過(guò)程。
這種方案中是否有漏洞?
最大的問(wèn)題是類(lèi)似[[Get]]的一些方法可能仍然返回可變對(duì)象,所以即使一些對(duì)象x是只讀視圖,x.prop可能是可變的!這是一個(gè)巨大的漏洞。
我們需要添加一個(gè)handler.get()方法來(lái)堵上漏洞:
var handler = {
...
// 在只讀視圖中包裹其它結(jié)果。
get: function (target, key, receiver) {
// 從執(zhí)行默認(rèn)行為開(kāi)始。
var result = Reflect.get(target, key, receiver);
// 確保返回一個(gè)不可變對(duì)象!
if (Object(result) === result) {
// result是一個(gè)對(duì)象。
return readOnlyView(result);
}
// result是一個(gè)原始原始類(lèi)型,所以已經(jīng)具備不可變的性質(zhì)。
return result;
},
...
};
這仍然不夠,getPrototypeOf和getOwnPropertyDescriptor這兩個(gè)方法也需要進(jìn)行同樣的處理。
然而還有更多問(wèn)題,當(dāng)通過(guò)這種代理調(diào)用getter或方法時(shí),傳遞給getter或方法的this的值通常是代理自身。但是正如我們之前所見(jiàn),有時(shí)代理無(wú)法通過(guò)訪問(wèn)器和方法執(zhí)行的類(lèi)型檢查。在這里用目標(biāo)對(duì)象代替代理更好一些。聰明的小伙伴,你知道如何解決這個(gè)問(wèn)題么?
由此可見(jiàn),創(chuàng)建代理非常簡(jiǎn)單,但是創(chuàng)建一個(gè)具有直觀行為的代理相當(dāng)困難。
只言片語(yǔ)
- 代理到底好在哪里?代理可以幫助你觀察或記錄對(duì)象訪問(wèn),當(dāng)調(diào)試代碼時(shí)助你一臂之力,測(cè)試框架也可以用代理來(lái)創(chuàng)建模擬對(duì)象(mock object)。代理可以幫助你強(qiáng)化普通對(duì)象的能力,例如:惰性屬性填充。我不太想提到這一點(diǎn),但是如果要想了解代理在代碼中的運(yùn)行方式,將代理的句柄對(duì)象包裹在另一個(gè)代理中是一個(gè)非常不錯(cuò)的辦法,每當(dāng)句柄方法被訪問(wèn)時(shí)就可以將你想要的信息輸出到控制臺(tái)中。正如上文中只讀視圖的示例readOnlyView,我們可以用代理來(lái)限制對(duì)象的訪問(wèn)。當(dāng)然在應(yīng)用代碼中很少遇到這種用例,但是Firefox在內(nèi)部使用代理來(lái)實(shí)現(xiàn)不同域名之間的安全邊界,是我們的安全模型的關(guān)鍵組成部分。
- 與WeakMap深度結(jié)合。在我們的readOnlyView示例中,每當(dāng)對(duì)象被訪問(wèn)的時(shí)候創(chuàng)建一個(gè)新的代理。這種做法可以幫助我們節(jié)省在WeakMap中創(chuàng)建代理時(shí)的緩存內(nèi)存,所以無(wú)論傳遞多少次對(duì)象給readOnlyView,只會(huì)創(chuàng)建一個(gè)代理。這也是一個(gè)動(dòng)人的WeakMap用例。
- 代理可解除。ES6規(guī)范中還定義了另外一個(gè)函數(shù):Proxy.revocable(target, handler)。這個(gè)函數(shù)可以像new Proxy(target, handler)一樣創(chuàng)建代理,但是創(chuàng)建好的代理后續(xù)可被解除。(Proxy.revocable方法返回一個(gè)對(duì)象,該對(duì)象有一個(gè).proxy屬性和一個(gè).revoke方法。)一旦代理被解除,它即刻停止運(yùn)行并拋出所有內(nèi)部方法。
- 對(duì)象不變性。在某些情況下,ES6需要代理的句柄方法來(lái)報(bào)告與目標(biāo)對(duì)象狀態(tài)一致的結(jié)果,以此來(lái)保證所有對(duì)象甚至是代理的不變性。舉個(gè)例子,除非目標(biāo)不可擴(kuò)展(inextensible),否則代理不能被聲明為不可擴(kuò)展的。
不變性的規(guī)則非常復(fù)雜,在此不展開(kāi)詳述,但是如果你看到類(lèi)似“proxy can't report a non-existent property as non-configurable”這樣的錯(cuò)誤信息,就可以考慮從不變性的角度解決問(wèn)題,最可能的補(bǔ)救方法是改變代理報(bào)告本身,或者在運(yùn)行時(shí)改變目標(biāo)對(duì)象來(lái)反射代理的報(bào)告指向。
現(xiàn)在,你認(rèn)為對(duì)象是什么?
我記得我們之前的見(jiàn)解是:“對(duì)象是屬性的集合。”
我不喜歡這個(gè)定義,即使給定義疊加原型和可調(diào)用能力也不會(huì)讓我改變看法。我認(rèn)為“集合(collection)”這個(gè)詞太危險(xiǎn)了,不適合用作對(duì)象的定義。對(duì)象的句柄方法可以做任何事情,它們也可以返回隨機(jī)結(jié)果。
ECMAScript標(biāo)準(zhǔn)委員會(huì)針對(duì)這個(gè)問(wèn)題開(kāi)展了許多研究,搞清楚了對(duì)象能做的事情,將那些方法進(jìn)行標(biāo)準(zhǔn)化,并將虛擬化技術(shù)作為每個(gè)人都能使用的一等特性添加到語(yǔ)言的新標(biāo)準(zhǔn)中,為前端開(kāi)發(fā)領(lǐng)域拓展了無(wú)限可能。
完善后的對(duì)象幾乎可以表示任何事物。
對(duì)象是什么?可能現(xiàn)在最貼切的答案需要用12個(gè)內(nèi)部方法進(jìn)行定義:對(duì)象是在JS程序中擁有[[Get]]、[[Set]]等操作的實(shí)體。
我不太確定我們是否比之前更了解對(duì)象,但是我們絕對(duì)做了許多驚艷的事情,是的,我們實(shí)現(xiàn)了舊版JS根本做不到的功能。
我現(xiàn)在可以使用代理么?
不!在Web平臺(tái)上無(wú)論如何都不行。目前只有Firefox和微軟的Edge支持代理,而且還沒(méi)有支持這一特性polyfill。
如果你想在Node.js或io.js環(huán)境中使用代理,首先你需要添加名為harmony-reflect的polyfill,然后在執(zhí)行時(shí)啟用一個(gè)非默認(rèn)的選項(xiàng)(--harmony_proxies),這樣就可以暫時(shí)使用V8中實(shí)現(xiàn)的老版本代理規(guī)范。
放輕松,讓我們一起來(lái)做試驗(yàn)吧!為每一個(gè)對(duì)象創(chuàng)建成千上萬(wàn)個(gè)相似的副本鏡像卻不能調(diào)試?現(xiàn)在就解放自己!不過(guò)目前來(lái)看,請(qǐng)不要將欠考慮的有關(guān)代理的代碼泄露到產(chǎn)品中,這非常危險(xiǎn)。
代理特性在2010年由Andreas Gal首先實(shí)現(xiàn),由Blake Kaplan進(jìn)行代碼審查。標(biāo)準(zhǔn)委員會(huì)后來(lái)完全重新設(shè)計(jì)了這個(gè)特性。Eddy Bruel在2012年實(shí)現(xiàn)了新標(biāo)準(zhǔn)。
我實(shí)現(xiàn)了反射(Reflect)特性,由Jeff Walden進(jìn)行代碼審查。Firefox Nightly已經(jīng)支持除Reflect.enumerate()外的所有特性