最新的JavaScript核心語言標準——ES6,完全改變你編寫JS代碼的方式!

原文地址javascript

遷移到:http://www.bdata-cap.com/newsinfo/1741515.htmlcss

本文內容

  • ECMAScript 發生了什麼變化?
  • 新標準
  • 版本號6
  • 兌現承諾
  • 迭代器和for-of循環
  • 生成器 Generators
  • 模板字符串
  • 不定參數和默認參數
  • 解構 Destructuring
  • 箭頭函數 Arrow Functions
  • Symbols
  • 集合
  • 學習Babel和Broccoli,立刻就用ES6
  • 代理 Proxies

ES6 說本身的宗旨是「凡是新加入的特性,勢必已在其它語言中獲得強有力的實用性證實。」——TRUE!若是你大概瀏覽下 ES6 的新特性,事實上它們都不是什麼新東西,而是在其餘語言中已經被普遍承認和採用的,還有就是多年工程實踐的結果,好比,JavaScript 框架 jQuery、Undercore、AnjularJS、Backbone、React、Ember、Polymer、Knockout 和 Browserify、RequireJS、Webpack,以及NPM 和 Bower,涉及到 JavaScript 的庫和框架、模塊打包器及測試、任務調度器、包和工做流管理等方面,之前須要用這些三方框架來實現,有些如今則不用了。由於,ES6 自己就具有。因此,之後寫 JS 代碼,或多或少跟像 Java、C# 等這些服務器端語言有點像~html

若是你嫌內容太長,能夠大概瀏覽一下也行~你會發現服務器編程語言不少特性,如今在前端也能使用了~前端

ECMAScript 發生了什麼變化?


JavaScript是ECMAScript的實現和擴展,由ECMA(一個相似W3C的標準組織)參與進行標準化。ECMAScript定義了:html5

  • 語言語法 – 語法解析規則、關鍵字、語句、聲明、運算符等。
  • 類型 – 布爾型、數字、字符串、對象等。
  • 原型和繼承
  • 內建對象和函數的標準庫 – JSON、Math、數組方法、對象自省方法等。

ECMAScript標準不定義HTML或CSS的相關功能,也不定義相似DOM(文檔對象模型)的Web API,這些都在其餘的標準中定義。java

ECMAScript涵蓋了各類環境中JS的使用場景,不管是瀏覽器環境仍是相似node.js的非瀏覽器環境。node

新標準


2015年6月,ECMAScript語言規範第6版最終草案提請Ecma大會審查,這意味着什麼呢?——咱們將迎來最新的JavaScript核心語言標準python

早在2009年,上一版的ES5,自那時起,ES標準委員會一直在緊鑼密鼓地籌備新的JS語言標準——ES6。jquery

ES6是一次重大的版本升級,與此同時,因爲ES6秉承着最大化兼容已有代碼的設計理念,你過去編寫的JS代碼將繼續正常運行。事實上,許多瀏覽器已經支持部分ES6特性,並將繼續努力實現其他特性。這意味着,在一些已經實現部分特性的瀏覽器中,你的JS代碼已經能夠正常運行。若是到目前爲止你還沒有遇到任何兼容性問題,那麼你頗有可能將不會遇到這些問題,瀏覽器正飛速實現各類新特性。git

版本號6


 

ECMAScript標準的歷史版本分別是一、二、三、5。

爲何沒有版4?其實,的確曾經計劃發佈具備大量新特性的版4,但最終因想法太過激進而慘遭廢除(這一版標準中曾經有一個極其複雜的支持泛型和類型推斷的內建靜態類型系統)。步子不能邁得太大~

ES4飽受爭議,當標準委員會最終中止開發ES4時,其成員贊成發佈一個相對謙和的ES5版本,隨後繼續制定一些更具實質性的新特性。這一明確的協商協議最終命名爲「Harmony」,所以,ES5規範中包含這樣兩句話:

ECMAScript是一門充滿活力的語言,並在不斷進化中。

將來版本的規範中將持續進行重要的技術改進。

兌現承諾


2009年的版5,引入了Object.create()、Object.defineProperty()、getters setters、嚴格模式以及JSON對象。我已經使用過全部這些新特性,而且很是喜歡。但這些改進並無影響我編寫JS代碼的方式,對我來講,最大的革新就是新的數組方法:.map()、. filter()

但ES6並不是如此!通過持續幾年的磨礪,它已成爲JS有史以來最實質的升級,新的語言和庫特性就像無主之寶,等待有識之士的發掘。新特性涵蓋範圍甚廣,小到受歡迎的語法糖,例如箭頭函數(arrow functions)和簡單的字符串插值(string interpolation),大到燒腦的新概念,例如代理(proxies)和生成器(generators)。

ES6將完全改變你編寫JS代碼的方式!

下面從一個經典的「遺漏特性」提及,十年來我一直期待在JavaScript中看到的它——ES6迭代器(iterators)和新的for-of循環!

迭代器和for-of循環


如何遍歷數組?20年前JavaScript剛萌生時,你可能這樣實現數組遍歷:

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

自 ES5 正式發佈後,你能夠使用內建的 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循環體除了遍歷數組元素外,還會遍歷自定義屬性。舉個例子,若是你的數組中有一個可枚舉屬性 myArray.name,循環將額外執行一次,遍歷到名爲「name」的索引。就連數組原型鏈上的屬性都能被訪問到。
  • 最讓人震驚的是,在某些狀況下,這段代碼可能按照隨機順序遍歷數組元素。
  • 簡而言之,for-in是爲普通對象設計的,你能夠遍歷獲得字符串類型的鍵,所以不適用於數組遍歷。

強大的for-of循環

目前來看,成千上萬的Web網站依賴 for-in 循環,其中一些網站甚至將其用於數組遍歷。若是想經過修正for-in循環增長數組遍歷支持會讓這一切變得更加混亂,所以,標準委員會在ES6中增長了一種新的循環語法來解決目前的問題。像下面那樣:

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

是的,與以前的內建方法相比,這種循環方式看起來是否有些眼熟?那好,咱們將要探究一下 for-of 循環的外表下隱藏着哪些強大的功能。如今,只需記住:

  • 這是最簡潔、最直接的遍歷數組元素的語法;
  • 與forEach()不一樣的是,它能夠正確響應 break、continuereturn 語句;
  • 這個方法避開了for-in循環的全部缺陷。

for-in循環用來遍歷對象屬性。for-of循環用來遍歷數據—例如數組中的值。

可是,不只如此!

for-of循環也能夠遍歷其它的集合。for-of循環不只支持數組,還支持大多數類數組對象,例如DOM 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 中的 foreach 循環語句。與它們同樣,for-of循環支持語言和標準庫中提供的幾種不一樣的數據結構。它一樣也是這門語言中的一個擴展點。

正如其它語言中的for/foreach語句同樣,for-of循環語句經過方法調用來遍歷各類集合。數組、Maps對象、Sets對象以及其它在咱們討論的對象有一個共同點,它們都有一個迭代器方法。

你能夠給任意類型的對象添加迭代器方法。

當你爲對象添加myObject.toString()方法後,就能夠將對象轉化爲字符串,一樣地,當你向任意對象添加myObject[Symbol.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) {
  // do something
}

而後是一個使用如下方法和少量臨時變量實現的與以前大體至關的示例:

 
var $iterator = ITERABLE[Symbol.iterator]();
var $result = $iterator.next();
while (!$result.done) {
   VAR = $result.value;
   // do something
   $result = $iterator.next();
 }

 

 

 

這段代碼沒有展現.return()方法是如何處理的,咱們能夠添加這部分代碼,但我認爲這對於咱們正在講解的內容來講過於複雜了。for-of循環用起來很簡單,可是其背後有着很是複雜的機制。

我什麼時候能夠開始使用這一新特性?

目前,對於for-of循環新特性,全部最新版本Firefox都(部分)支持(譯註:從FF 13開始陸續支持相關功能,FF 36 - FF 40基本支持大部分特性),在Chrome中能夠經過訪問 chrome://flags 並啓用「實驗性JavaScript」來支持。微軟的Spartan瀏覽器支持,可是IE不支持。若是你想在web環境中使用這種新語法,同時須要支持 IE和Safari,你能夠使用Babel或Google的Traceur這些編譯器來將你的ES6代碼翻譯爲Web友好的ES5代碼。

而在服務端,你不須要相似的編譯器,io.js中默認支持ES6新語法(部分),在Node中須要添加--harmony選項來啓用相關特性。

{done: true}

for-of 循環的使用遠沒有結束。

在ES6中有一種新的對象與for-of循環配合使用很是契合,後面將講解。我認爲這種新特性是ES6種最夢幻的地方——ES6 的生成器:generators,若是你還沒有在相似Python和C#的語言中遇到它,你一開始極可能會發現它使人難以置信,可是這是編寫迭代器最簡單的方式,在重構中很是有用,而且它極可能改變咱們書寫異步代碼的方式,不管是在瀏覽器環境仍是服務器環境 。

生成器 Generators


爲何說是「最具魔力的」?對於初學者來講,此特性與JS以前已有的特性大相徑庭,可能會以爲有點晦澀難懂。可是,從某種意義上來講,它使語言內部的常態行爲變得更增強大,若是這都不算有魔力,我不知道還有什麼能算。

不只如此,此特性能夠極大地簡化代碼,它甚至能夠幫助你逃離「回調地獄」。

既然新特性如此神奇,那麼就一塊兒深刻了解它的魔力吧!

什麼是生成器?

咱們從一個示例開始:

function* quips(name) {
  yield "你好 " + name + "!";
  yield "但願你能喜歡這篇介紹ES6的譯文";
  if (name.startsWith("X")) {
    yield "你的名字 " + name + "  首字母是X,這很酷!";
  }
  yield "咱們下次再見!";
}

這是一隻會說話的貓,這段代碼極可能表明着當今互聯網上最重要的一類應用。(試着點擊這個連接,與這隻貓互動一下,若是你感到有些困惑,回到這裏繼續閱讀)。

這段代碼看起來很像一個函數,咱們稱之爲生成器函數,它與普通函數有不少共同點,可是兩者有以下區別:

  • 普通函數使用function聲明,而生成器函數使用function*聲明。
  • 在生成器函數內部,有一種相似return的語法:關鍵字yield。兩者的區別是,普通函數只能夠return一次,而生成器函數能夠yield屢次(固然也能夠只yield一次)。在生成器的執行過程當中,遇到yield表達式當即暫停,後續可恢復執行狀態。

這就是普通函數和生成器函數之間最大的區別,普通函數不能自暫停,生成器函數能夠。

生成器作了什麼?

當你調用quips()生成器函數時發生了什麼?

> var iter = quips("jorendorff");
  [object Generator]
> iter.next()
  { value: "你好 jorendorff!", done: false }
> iter.next()
  { value: "但願你能喜歡這篇介紹ES6的譯文", done: false }
> iter.next()
  { value: "咱們下次再見!", done: false }
> iter.next()
  { value: undefined, done: true }

你大概已經習慣了普通函數的使用方式,當你調用它們時,它們當即開始運行,直到遇到return或拋出異常時才退出執行,做爲JS程序員你必定深諳此道。

生成器調用看起來很是相似:quips("jorendorff")。可是,當你調用一個生成器時,它並不是當即執行,而是返回一個已暫停的生成器對象(上述實例代碼中的iter)。你可將這個生成器對象視爲一次函數調用,只不過當即凍結了,它剛好在生成器函數的最頂端的第一行代碼以前凍結了。

每當你調用生成器對象的.next()方法時,函數調用將其自身解凍並一直運行到下一個yield表達式,再次暫停。

這也是在上述代碼中咱們每次都調用iter.next()的緣由,咱們得到了quips()函數體中yield表達式生成的不一樣的字符串值。

調用最後一個iter.next()時,咱們最終抵達生成器函數的末尾,因此返回結果中done的值爲true。抵達函數的末尾意味着沒有返回值,因此返回結果中value的值爲undefined。

如今回到會說話的貓的demo頁面,嘗試在循環中加入一個yield,會發生什麼?

若是用專業術語描述,每當生成器執行yields語句,生成器的堆棧結構(本地變量、參數、臨時值、生成器內部當前的執行位置)被移出堆棧。然而,生成器對象保留了對這個堆棧結構的引用(備份),因此稍後調用.next()能夠從新激活堆棧結構而且繼續執行。

值得特別一提的是,生成器不是線程,在支持線程的語言中,多段代碼能夠同時運行,統統常致使競態條件和非肯定性,不過同時也帶來不錯的性能。生成器則徹底不一樣。當生成器運行時,它和調用者處於同一線程中,擁有肯定的連續執行順序,永不併發。與系統線程不一樣的是,生成器只有在其函數體內標記爲yield的點纔會暫停。

如今,咱們瞭解了生成器的原理,領略過生成器的運行、暫停恢復運行的不一樣狀態。那麼,這些奇怪的功能究竟有何用處?

生成器是迭代器!

上週,咱們學習了ES6的迭代器,它是ES6中獨立的內建類,同時也是語言的一個擴展點,經過實現[Symbol.iterator]()和.next()兩個方法你就能夠建立自定義迭代器。

實現一個接口不是一樁小事,咱們一塊兒實現一個迭代器。舉個例子,咱們建立一個簡單的range迭代器,它能夠簡單地將兩個數字之間的全部數相加。首先是傳統C的for(;;)循環:

// 應該彈出三次 "ding"
for (var value of range(0, 3)) {
  alert("Ding! at floor #" + value);
}

使用ES6的類的解決方案(若是不清楚語法細節,無須擔憂,咱們將在接下來的文章中爲你講解):

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};
    }
  }
}
 
// 返回一個新的迭代器,能夠從start到stop計數。
function range(start, stop) {
  return new RangeIterator(start, stop);
}

查看代碼運行狀況。

這裏的實現相似JavaSwift中的迭代器,不是很糟糕,但也不是徹底沒有問題。咱們很難說清這段代碼中是否有bug,這段代碼看起來徹底不像咱們試圖模仿的傳統for (;;)循環,迭代器協議迫使咱們拆解掉循環部分。

此時此刻你對迭代器可能尚無感受,他們用起來很酷,但看起來有些難以實現。

你大概不會爲了使迭代器更易於構建從而建議咱們爲JS語言引入一個離奇古怪又野蠻的新型控制流結構,可是既然咱們有生成器,是否能夠在這裏應用它們呢?一塊兒嘗試一下:

function* range(start, stop) {
  for (var i = start; i < stop; i++)
    yield i;
}

查看代碼運行狀況。

以上4行代碼實現的生成器徹底能夠替代以前引入了一整個RangeIterator類的23行代碼的實現。可行的緣由是:生成器是迭代器。全部的生成器都有內建.next()和[Symbol.iterator]()方法的實現。你只須編寫循環部分的行爲。

咱們都很是討厭被迫用被動語態寫一封很長的郵件,不借助生成器實現迭代器的過程與之相似,使人痛苦不堪。當你的語言再也不簡練,說出的話就會變得難以理解。RangeIterator的實現代碼很長而且很是奇怪,由於你須要在不借助循環語法的前提下爲它添加循環功能的描述。因此生成器是最好的解決方案!

咱們如何發揮做爲迭代器的生成器所產生的最大效力?

l 使任意對象可迭代。編寫生成器函數遍歷這個對象,運行時yield每個值。而後將這個生成器函數做爲這個對象的[Symbol.iterator]方法。

l 簡化數組構建函數。假設你有一個函數,每次調用的時候返回一個數組結果,就像這樣:

// 拆分一維數組icons
// 根據長度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;
}

使用生成器建立的代碼相對較短:

function* splitIntoRows(icons, rowLength) {
  for (var i = 0; i < icons.length; i += rowLength) {
    yield icons.slice(i, i + rowLength);
  }
}

行爲上惟一的不一樣是,傳統寫法當即計算全部結果並返回一個數組類型的結果,使用生成器則返回一個迭代器,每次根據須要逐一地計算結果。

  • 獲取異常尺寸的結果。你沒法構建一個無限大的數組,可是你能夠返回一個能夠生成一個永無止境的序列的生成器,每次調用能夠從中取任意數量的值。
  • 重構複雜循環。你是否寫過又醜又大的函數?你是否願意將其拆分爲兩個更簡單的部分?如今,你的重構工具箱裏有了新的利刃——生成器。當你面對一個複雜的循環時,你能夠拆分出生成數據的代碼,將其轉換爲獨立的生成器函數,而後使用for (var data of myNewGenerator(args))遍歷咱們所需的數據。
  • 構建與迭代相關的工具。ES6不提供用來過濾、映射以及針對任意可迭代數據集進行特殊操做的擴展庫。藉助生成器,咱們只須寫幾行代碼就能夠實現相似的工具。

舉個例子,假設你須要一個等效於Array.prototype.filter而且支持DOM NodeLists的方法,能夠這樣寫:

function* filter(test, iterable) {
  for (var item of iterable) {
    if (test(item))
      yield item;
  }
}

你看,生成器魔力四射!藉助它們的力量能夠很是輕鬆地實現自定義迭代器,記住,迭代器貫穿ES6的始終,它是數據和循環的新標準。

以上只是生成器的冰山一角,最重要的功能請繼續觀看!

生成器和異步代碼

這是我之前寫的一些JS代碼:

       };
      })
     }); 
    });
  });
});

可能你已經見過相似的代碼,異步API一般須要一個回調函數,這意味着你須要爲每一次任務執行編寫額外的異步函數。因此若是你有一段代碼須要完成三個任務,你將看到相似的三層級縮進的代碼,而非簡單的三行代碼。

後來我就這樣寫了:

 

 

 

 

 

 

 

 

 

}).on('close', function () {
  done(undefined, undefined);
}).on('error', function (error) {
  done(error);
});

異步API擁有錯誤處理規則,不支持異常處理。不一樣的API有不一樣的規則,大多數的錯誤規則是默認的;在有些API裏,甚至連成功提示都是默認的。

這些是到目前爲止咱們爲異步編程所付出的代價,咱們正慢慢開始接受異步代碼不如等效同步代碼美觀又簡潔的這個事實。

生成器爲你提供了避免以上問題的新思路。

實驗性的Q.async()嘗試結合promises使用生成器產生異步代碼的等效同步代碼。舉個例子:

// 製造一些噪音的同步代碼。
function makeNoise() {
  shake();
  rattle();
  roll();
}
 
// 製造一些噪音的異步代碼。
// 返回一個Promise對象
// 當咱們製造完噪音的時候會變爲resolved
function makeNoise_async() {
  return Q.async(function* () {
    yield shake_async();
    yield rattle_async();
    yield roll_async();
  });
}

兩者主要的區別是,異步版必須在每次調用異步函數的地方添加yield關鍵字。

在Q.async版本中添加一個相似if語句的判斷或try/catch塊,如同向同步版本中添加相似功能同樣簡單。與其它異步代碼編寫方法相比,這種方法更天然,不像學一門新語言同樣辛苦。

若是你已經看到這裏,你能夠試着閱讀來自James Long的更深刻地講解生成器的文章

生成器爲咱們提供了一個新的異步編程模型思路,這種方法更適合人類的大腦。相關工做正在不斷展開。此外,更好的語法或許會有幫助,ES7中有一個有關異步函數的提案,它基於promises和生成器構建,並從C#類似的特性中汲取了大量靈感。

如何應用這些瘋狂的新特性?

在服務器端,如今你能夠在io.js中使用ES6(在Node中你須要使用 –harmony 這個命令行選項)。

在瀏覽器端,到目前爲止只有Firefox 27+和Chrome 39+支持了ES6生成器。若是要在web端使用生成器,你須要使用BabelTraceur來將你的ES6代碼轉譯爲Web友好的ES5。

起初,JS中的生成器由Brendan Eich實現,他的設計參考了Python生成器,而此Python生成器則受到Icon的啓發。他們早在2006年就在Firefox 2.0中移植了相關代碼。可是,標準化的道路崎嶇不平,相關語法和行爲都在原先的基礎上有所改動。Firefox和Chrome中的ES6生成器都是由編譯器hacker Andy Wingo實現的。這項工做由Bloomberg贊助支持(沒聽錯,就是大名鼎鼎的那個彭博!)。

生成器還有更多未說起的特性,例如:.throw()和.return()方法、可選參數.next()、yield*表達式語法。因爲行文過長,估計觀衆已然疲乏,咱們應該學習一下生成器,暫時yield在這裏,剩下的乾貨擇機爲你們獻上。

下一次,咱們變換一下風格,因爲咱們接連搬了兩座大山:迭代器和生成器,下次就一塊兒研究下不會改變你編程風格的ES6特性好不?就是一些簡單又實用的東西,你必定會喜笑顏開噠!你還別說,在什麼都要「微」一下的今天,ES6固然要有微改進了!

續篇

回顧

在第三篇文章中,咱們着重講解了生成器的基本行爲。你可能對此感到陌生,可是並不難理解。生成器函數與普通函數有不少類似之處,它們之間最大的不一樣是,普通函數一次執行完畢,而生成器函數體每次執行一部分,每當執行到一個yield表達式的時候就會暫停。

儘管在那篇文章中咱們進行過詳細解釋,但咱們始終未把全部特性結合起來給你們講解示例。如今就讓咱們出發吧!

function* somewords() {
  yield "hello";
  yield "world";
}
for (var word of somewords()) {
  alert(word);
}

這段腳本簡單易懂,可是若是你把代碼中不一樣的比特位當作戲劇中的任務,你會發現它變得如此不同凡響。穿上新衣的代碼看起來是這樣的:

(譯者注:下面這是原做者創做的一個劇本,他將ES6中的各類函數和語法擬人化,以講解生成器(Generator)的實現原理)

場景 - 另外一個世界的計算機,白天

for loop女士獨自站在舞臺上,戴着一頂安全帽,手裏拿着一個筆記板,上面記載着全部的事情。

for loop:
                (電話響起)
                somewords()!

generator出現:這是一位高大的、有着一絲不苟紳士外表的黃銅機器人。
它看起來足夠友善,但給人的感受仍然是冷冰冰的金屬。

for loop:
            (瀟灑地拍了拍她的手)
          好吧!咱們去找些事兒作吧。
             (對generator說)
                  .next()!

generator動了起來,就像忽然擁有了生命。

generator:
       {value: "hello", done: false}

然而猝不及防的,它以一個滑稽的姿式中止了動做。

for loop:
                   alert!

alert小子飛快衝進舞臺,眼睛大睜,上氣不接下氣。咱們感受的到他一貫如此。

for loop:
             對user說「hello」。

alert小子轉身衝下舞臺。

alert:
            (舞臺下,大聲尖叫)
               一切都靜止了!
             你正在訪問的頁面說,
                  「hello」!

停留了幾秒鐘後,alert小子跑回舞臺,穿過全部人滑停在for loop女士身邊。

alert:
                 user說ok。
                  for loop:
           (瀟灑地拍了拍她的手)
          好吧!咱們去找些事兒作吧。
           (回到generator身邊)
                  .next()!

generator又一次煥發生機。

generator:
       {value: "world", done: false}

它換了個姿式又一次凍結。

for loop:
                   alert!
                   alert:
                (已經跑起來)
                  正在搞定!
              (舞臺下,大聲尖叫)
                一切都靜止了!
              你正在訪問的頁面說,
                  「world」!

又一次暫停,而後alert忽然跋涉回到舞臺,垂頭喪氣的。

alert:
            user再一次說ok,可是…
              可是請阻止這個頁面
               建立額外的對話。

他噘着嘴離開了。

for loop:
            (瀟灑地拍了拍她的手)
           好吧!咱們去找些事兒作吧。
            (回到generator身邊)
                  .next()!

generator第三次煥發生機。

generator:
                 (莊嚴的)
       {value: undefined, done: true}

它的頭低下了,光芒從它的眼裏消失。它再也不移動。

for loop
               個人午飯時間到了。

她離開了。

一下子,garbage collector(垃圾收集器)老頭進入,撿起了奄奄一息的generator,將它帶下舞臺。

好吧,這一齣戲不太像哈姆雷特,但你應該能夠想象得出來。

正如你在戲劇中看到的,當生成器對象第一次出現時,它當即暫停了。每當調用它的.next()方法,它都會甦醒並向前執行一部分。

全部動做都是單線程同步的。請注意,不管什麼時候永遠只有一個真正活動的角色,角色們不會互相打斷,亦不會互相討論,他們輪流講話,只要他們的話沒有說完均可以繼續說下去。(就像莎士比亞同樣!)

每當for-of循環遍歷生成器時,這齣戲的某個版本就展開了。這些.next()方法調用序列永遠不會在你的代碼的任何角落出現,在劇本里我把它們都放在舞臺上了,可是對於你和你的程序而言,全部這一切都應該在幕後完成,由於生成器和for-of循環就是被設計成經過迭代器接口聯結工做的。

因此,總結一下到目前爲止全部的一切:

  • 生成器對象是能夠產生值的優雅的黃銅機器人。
  • 每一個生成器函數體構成的單一代碼塊就是一個機器人。

如何關停生成器

我在第1部分沒有提到這些繁瑣的生成器特性:

  • generator.return()
  • generator.next()的可選參數
  • generator.throw(error)
  • yield*

若是你不理解這些特性存在得意義,就很難對它們提起興趣,更不用說理解它們的實現細節,因此我選擇直接跳過。可是當咱們深刻學習生成器時,勢必要仔細瞭解這些特性的方方面面。

你或許曾使用過這樣的模式:

function dothings() {
  setup();
  try {
    // ... 作一些事情
  } finally {
    cleanup();
  }
}
dothings();

清理(cleanup)過程包括關閉鏈接或文件,釋放系統資源,或者只是更新dom來關閉「運行中」的加載動畫。咱們但願不管任務成功完成與否都觸發清理操做,因此執行流入到finally代碼塊。

那麼生成器中的清理操做看起來是什麼樣的呢?

function* producevalues() {
  setup();
  try {
    // ... 生成一些值
  } finally {
    cleanup();
  }
}
for (var value of producevalues()) {
  work(value);
}

這段代碼看起來很好,可是這裏有一個問題:咱們沒在try代碼塊中調用work(value),若是它拋出異常,咱們的清理步驟會如何執行呢?

或者假設for-of循環包含一條break語句或return語句。清理步驟又會如何執行呢?

放心,清理步驟不管如何都會執行,ES6已經爲你作好了一切。

咱們第一次討論迭代器和for-of循環時曾說過,迭代器接口支持一個可選的.return()方法,每當迭代在迭代器返回{done:true}以前退出都會自動調用這個方法。生成器支持這個方法,mygenerator.return()會觸發生成器執行任一finally代碼塊而後退出,就好像當前的生成暫停點已經被祕密轉換爲一條return語句同樣。

注意,.return()方法並非在全部的上下文中都會被自動調用,只有當使用了迭代協議的狀況下才會觸發該機制。因此也有可能生成器沒執行finally代碼塊就直接被垃圾回收了。

如何在舞臺上模擬這些特性?生成器被凍結在一個須要一些配置的任務(例如,建造一幢摩天大樓)中間。忽然有人拋出一個錯誤!for循環捕捉到這個錯誤並將它放置在一遍,她告訴生成器執行.return()方法。生成器冷靜地拆除了全部腳手架並停工。而後for循環取回錯誤,繼續執行正常的異常處理過程。

生成器主導模式

到目前爲止,咱們在劇本中看到的生成器(generator)和使用者(user)之間的對話很是有限,如今換一種方式繼續解釋:

在這裏使用者主導一切流程,生成器根據須要完成它的任務,但這不是使用生成器進行編程的惟一方式。

在第1部分中我曾經說過,生成器能夠用來實現異步編程,完成你用異步回調或promise鏈所作的一切。我知道你必定想知道它是如何實現的,爲何yield的能力(這但是生成器專屬的特殊能力)足夠應對這些任務。畢竟,異步代碼不只產生(yield)數據,還會觸發事件,好比從文件或數據庫中調用數據,向服務器發起請求並返回事件循環來等待異步過程結束。生成器如何實現這一切?它又是如何不借助回調力量從文件、數據庫或服務器中接受數據?

爲了開始找出答案,考慮一下若是.next()的調用者只有一種方法能夠傳值返回給生成器會發生什麼?僅僅是這一點改變,咱們就可能創造一種全新的會話形式:

事實上,生成器的.next()方法接受一個可選參數,參數稍後會做爲yield表達式的返回值出如今生成器中。那就是說,yield語句與return語句不一樣,它是一個只有當生成器恢復時纔會有值的表達式。

var results = yield getdataandlatte(request.areacode);

這一行代碼完成了許多功能:

  • 調用getdataandlatte(),假設函數返回咱們在截圖中看到的字符串「get me the database records for area code...」。
  • 暫停生成器,生成字符串值。
  • 此時能夠暫停任意長的時間。
  • 最終,直到有人調用.next({data: ..., coffee: ...}),咱們將這個對象存儲在本地變量results中並繼續執行下一行代碼。

下面這段代碼完整地展現了這一行代碼完整的上下文會話:

function* handle(request) {
  var results = yield getdataandlatte(request.areacode);
  results.coffee.drink();
  var target = mosturgentrecord(results.data);
  yield updatestatus(target.id, "ready");
}

yield仍然保持着它的原始含義:暫停生成器,返回值給調用者。可是確實也發生了變化!這裏的生成器期待來自調用者的很是具體的支持行爲,就好像調用者是它的行政助理同樣。

普通函數則與之不一樣,一般更傾向於知足調用者的需求。可是你能夠藉助生成器創造一段對話,拓展生成器與其調用者之間可能存在的關係。

這個行政助理生成器運行器多是什麼樣的?它大可沒必要很複雜,就像這樣:

function rungeneratoronce(g, result) {
  var status = g.next(result);
  if (status.done) {
    return;  // phew!
  }
  // 生成器請咱們去獲取一些東西而且
  // 當咱們搞定的時候再回調它
  doasynchronousworkincludingespressomachineoperations(
    status.value,
    (error, nextresult) => rungeneratoronce(g, nextresult));
}

爲了讓這段代碼運行起來,咱們必須建立一個生成器而且運行一次,像這樣:

rungeneratoronce(handle(request), undefined);

在以前的文章中,我一個庫的示例中提到Q.async(),在那個庫中,生成器是能夠根據須要自動運行的異步過程。rungeneratoronce正式這樣的一個具體實現。事實上,生成器通常會生成Promise對象來告訴調用者要作的事情,而不是生成字符串來大聲告訴他們。

若是你已經理解了Promise的概念,如今又理解了生成器的概念,你能夠嘗試修改rungeneratoronce的代碼來支持Promise。這個任務不簡單,可是一旦成功,你將可以用Promise線性書寫複雜的異步算法,而不只僅經過.then()方法或回調函數來實現異步功能。

如何銷燬生成器

你是否有看到rungeneratoronce的錯誤處理過程?答案必定是沒有,由於上面的示例中直接忽略了錯誤!

是的,那樣作很差,可是若是咱們想要以某種方法給生成器報告錯誤,能夠嘗試一下這個方法:當有錯誤產生時,不要繼續調用generator.next(result)方法,而應該調用generator.throw(error)方法來拋出yield表達式,進而像.return()方法同樣終止生成器的執行。可是若是當前的生成暫停點在一個try代碼塊中,那麼會catch到錯誤並執行finally代碼塊,生成器就恢復執行了。

另外一項艱鉅的任務來啦,你須要修改rungeneratoronce來確保.throw()方法可以被恰當地調用。請記住,生成器內部拋出的異常老是會傳播到調用者。因此不管生成器是否捕獲錯誤,generator.throw(error)都會拋出error並當即返回給你。

當生成器執行到一個yield表達式並暫停後能夠實現如下功能:

  • 調用generator.next(value),生成器從離開的地方恢復執行。
  • 調用generator.return(),傳遞一個可選值,生成器只執行finally代碼塊並再也不恢復執行。
  • 調用generator.throw(error),生成器表現得像是yield表達式調用一個函數並拋出錯誤。
  • 或者,什麼也不作,生成器永遠保持凍結狀態。(是的,對於一個生成器來講,極可能執行到一個try代碼塊,永不執行finally代碼塊。這種狀態下的生成器能夠被垃圾收集器回收。)

看起來生成器函數與普通函數的複雜度至關,只有.return()方法顯得不太同樣。

事實上,yield與函數調用有許多共通的地方。當你調用一個函數,你就暫時中止了,對不對?你調用的函數取得主導權,它可能返回值,可能拋出錯誤,或者永遠循環下去。

結合生成器實現更多功能

我再展現一個特性。假設咱們寫一個簡單的生成器函數聯結兩個可迭代對象:

function* concat(iter1, iter2) {
  for (var value of iter1) {
    yield value;
  }
  for (var value of iter2) {
    yield value;
  }
}

es6支持這樣的簡寫方式:

function* concat(iter1, iter2) {
  yield* iter1;
  yield* iter2;
}

普通yield表達式只生成一個值,而yield*表達式能夠經過迭代器進行迭代生成全部的值。

這個語法也能夠用來解決另外一個有趣的問題:在生成器中調用生成器。在普通函數中,咱們能夠從將一個函數重構爲另外一個函數並保留全部行爲。很顯然咱們也想重構生成器,但咱們須要一種調用提取出來的子例程的方法,咱們還須要確保,子例程可以生成以前生成的每個值。yield*能夠幫助咱們實現這一目標。

function* factoredoutchunkofcode() { ... }
function* refactoredfunction() {
  ...
  yield* factoredoutchunkofcode();
  ...
}

考慮一下這樣一個場景:一個黃銅機器人將子任務委託給另外一個機器人,函數對組織同步代碼來講相當重要,因此這種思想能夠使基於生成器特性的大型項目保持簡潔有序。

模板字符串


反撇號(`)基礎知識

ES6引入了一種新型的字符串字面量語法,咱們稱之爲模板字符串(template strings)。除了使用反撇號字符 ` 代替普通字符串的引號 ' 或 " 外,它們看起來與普通字符串並沒有二致。在最簡單的狀況下,它們與普通字符串的表現一致:

context.fillText(`Ceci n'est pas une chaîne.`, x, y);

但咱們不能說:「原來只是被反撇號括起來的普通字符串啊」。模板字符串爲JavaScript提供了簡單的字符串插值功能,今後之後,你能夠經過一種更加美觀、更加方便的方式向字符串中插值了。這在 Java 和 C# 中早已經有了,不用再用 + 符號鏈接字符串,用起來很方便~

模板字符串的使用方式成千上萬,但最讓我暖心的是將其應用於絕不起眼的錯誤消息提示:

function authorize(user, action) {
  if (!user.hasPrivilege(action)) {
    throw new Error(
      `用戶 ${user.name} 未被受權執行 ${action} 操做。`);
  }
}

在這個示例中,${user.name} ${action} 被稱爲模板佔位符,JavaScript將把user.name和action的值插入到最終生成的字符串中,例如:用戶jorendorff未被受權打冰球。(這是真的,我尚未得到冰球許可證。)

到目前爲止,咱們所瞭解到的僅僅是比 + 運算符更優雅的語法,下面是你可能期待的一些特性細節:

  • 模板佔位符中的代碼能夠是任意JavaScript表達式,因此函數調用、算數運算等這些均可以做爲佔位符使用,你甚至能夠在一個模板字符串中嵌套另外一個,我稱之爲模板套構(template inception)。
  • 若是這兩個值都不是字符串,能夠按照常規將其轉換爲字符串。例如:若是action是一個對象,將會調用它的.toString()方法將其轉換爲字符串值。
  • 若是你須要在模板字符串中書寫反撇號,你必須使用反斜槓將其轉義:`\``等價於"`"。
  • 一樣地,若是你須要在模板字符串中引入字符$和{。不管你要實現什麼樣的目標,你都須要用反斜槓轉義每個字符:`\$`和`\{`。

與普通字符串不一樣的是,模板字符串能夠多行書寫:

$("#warning").html(`
  <h1>當心!>/h1>
  <p>未經受權打冰球可能受罰
  將近${maxPenalty}分鐘。</p>
`);

模板字符串中全部的空格、新行、縮進,都會原樣輸出在生成的字符串中。

好啦,我說過要讓大家輕鬆掌握模板字符串,從如今起難度會加大,你能夠到此爲止,去喝一杯咖啡,慢慢消化以前的知識。真的,及時回頭不是一件使人感到羞愧的事情。Lopes Gonçalves曾經向咱們證實過,船隻不會被海妖碾壓,也不會從地球的邊緣墜落下去,他最終跨越了赤道,可是他有繼續探索整個南半球麼?並無,他回家了,吃了一頓豐盛的午飯,你必定不排斥這樣的感受。

反撇號的將來

固然,模板字符串也並不是事事包攬:

  • 它們不會爲你自動轉義特殊字符,爲了不跨站腳本漏洞,你應當像拼接普通字符串時作的那樣對非置信數據進行特殊處理。
  • 它們沒法很好地與國際化庫(能夠幫助你面向不一樣用戶提供不一樣的語言)相配合,模板字符串不會格式化特定語言的數字和日期,更別提同時使用不一樣語言的狀況了。
  • 它們不能替代模板引擎的地位,例如:MustacheNunjucks

模板字符串沒有內建循環語法,因此你沒法經過遍歷數組來構建相似HTML中的表格,甚至它連條件語句都不支持。你固然能夠使用模板套構(template inception)的方法實現,但在我看來這方法略顯愚鈍啊。

不過,ES6爲JS開發者和庫設計者提供了一個很好的衍生工具,你能夠藉助這一特性突破模板字符串的諸多限制,咱們稱之爲標籤模板(tagged templates)。

標籤模板的語法很是簡單,在模板字符串開始的反撇號前附加一個額外的標籤便可。咱們的第一個示例將添加一個SaferHTML標籤,咱們要用這個標籤來解決上述的第一個限制:自動轉義特殊字符。

請注意,ES6標準庫不提供相似SaferHTML功能,咱們將在下面本身來實現這個功能。

var message =
  SaferHTML`<p>${bonk.sender} 向你示好。</p>`;

這裏用到的標籤是一個標識符SaferHTML;也能夠使用屬性值做爲標籤,例如:SaferHTML.escape;還能夠是一個方法調用,例如:SaferHTML.escape({unicodeControlCharacters: false})。精確地說,任何ES6的成員表達式(MemberExpression)或調用表達式(CallExpression)均可做爲標籤使用。

能夠看出,無標籤模板字符串簡化了簡單字符串拼接,標籤模板則徹底簡化了函數調用!

上面的代碼等效於:

var message =
  SaferHTML(templateData, bonk.sender);

templateData是一個不可變數組,存儲着模板全部的字符串部分,由JS引擎爲咱們建立。由於佔位符將標籤模板分割爲兩個字符串的部分,因此這個數組內含兩個元素,形如Object.freeze(["<p>", " has sent you a bonk.</p>"]。

(事實上,templateData中還有一個屬性,在這篇文章中咱們不會用到,可是它是標籤模板不可分割的一環:templateData.raw,它一樣是一個數組,存儲着標籤模板中全部的字符串部分,若是咱們查看源碼將會發現,在這裏是使用形如\n的轉義序列分行,而在templateData中則爲真正的新行,標準標籤String.raw會用到這些原生字符串。)

如此一來,SaferHTML函數就能夠有成千上萬種方法來解析字符串和佔位符。

在繼續閱讀之前,可能你苦苦思索到底用SaferHTML來作什麼,而後着手嘗試去實現它,歸根結底,它只是一個函數,你能夠在Firefox的開發者控制檯裏測試你的成果。

如下是一種可行的方案(在gist中查看):

function SaferHTML(templateData) {
  var s = templateData[0];
  for (var i = 1; i < arguments.length; i++) {
    var arg = String(arguments[i]);
 
    // 轉義佔位符中的特殊字符。
    s += arg.replace(/&/g, "&")
            .replace(/</g, "<")
            .replace(/</g, ">");
 
    // 不轉義模板中的特殊字符。
    s += templateData[i];
  }
  return s;
}

經過這樣的定義,標籤模板SaferHTML`<p>${bonk.sender} 向你示好。</p>` 可能擴展爲字符串 "<p>ES6<3er 向你示好。</p>"。即便一個惡意命名的用戶,例如「黑客Steve<script>alert('xss');< /script>」,向其餘用戶發送一條騷擾信息,不管如何這條信息都會被轉義爲普通字符串,其餘用戶不會受到潛在攻擊的威脅。

(順便一提,若是你感受上述代碼中在函數內部使用參數對象的方式令你感到枯燥乏味,不妨期待下一篇,ES6中的另外一個新特性必定會讓你眼前一亮!)

僅一個簡單的示例不足以說明標籤模板的靈活性,咱們一塊兒回顧下咱們以前有關模板字符串限制的列表,看一下你還能作些什麼不同的事情。

  • 模板字符串不會自動轉義特殊字符。可是正如咱們看到的那樣,經過標籤模板,你能夠本身寫一個標籤函數來解決這個問題。

事實上,你能夠作的比那更好。

站在安全角度來講,我實現的SaferHTML函數至關脆弱,你須要經過多種不一樣的方式將HTML不一樣部分的特殊字符轉義,SaferHTML就沒法作到所有轉義。可是稍加努力,你就能夠寫出一個更加智能的SaferHTML函數,它能夠針對templateData中字符串中的HTML位進行解析,分析出哪個佔位符是純HTML;哪個是元素內部屬性,須要轉義'和";哪個是URL的query字符串,須要進行URL轉義而非HTML轉義,等等。智能SaferHTML函數能夠將每一個佔位符都正確轉義。

HTML的解析速度很慢,這種方法聽起來是否略顯牽強?幸運的是,當模板從新求值的時候標籤模板的字符串部分是不改變的。SaferHTML能夠緩存全部的解析結果,來加速後續的調用。(緩存能夠按照ES6的另外一個特性——WeakMap的形式進行存儲,咱們將在將來的文章中繼續深刻討論。)

  • 模板字符串沒有內建的國際化特性,可是經過標籤,咱們能夠添加這些功能。Jack Hsu的一篇博客文章展現了具體的實現過程。我謹在此處拋磚引玉:
i18n`Hello ${name}, you have ${amount}:c(CAD) in your bank account.`
// => Hallo Bob, Sie haben 1.234,56 $CA auf Ihrem Bankkonto.

注意觀察這個示例中的運行細節,name和amount都是JavaScript,進行正常插值處理,可是有一段不同凡響的代碼,:c(CAD),Jack將它放入了模板的字符串部分。JavaScript理應由JavaScript引擎進行處理,字符串部分由Jack的 i18n標籤進行處理。使用者能夠經過i18n的文檔瞭解到,:c(CAD)表明加拿大元的貨幣單位。

這就是標籤模板的大部分實際應用了。

  • 模板字符串不能代替Mustache和Nunjucks,一部分緣由是在模板字符串沒有內建的循環或條件語句語法。咱們一塊兒來看如何解決這個問題,若是JS不提供這個特性,咱們就寫一個標籤來提供相應支持。
// 基於純粹虛構的模板語言
// ES6標籤模板。
var libraryHtml = hashTemplate`
  <ul>
    #for book in ${myBooks}
      <li><i>#{book.title}</i> by #{book.author}</li>
    #end
  </ul>
`;

標籤模板帶來的靈活性遠不止於此,要記住,標籤函數的參數不會自動轉換爲字符串,它們如返回值同樣,能夠是任何值,標籤模板甚至不必定要是字符串!你能夠用自定義的標籤來建立正則表達式、DOM樹、圖片、以promises爲表明的整個異步過程、JS數據結構、GL着色器……

標籤模板以開放的姿態歡迎庫設計者們來建立強有力的領域特定語言。這些語言可能看起來不像JS,可是它們仍能夠無縫嵌入到JS中並與JS的其它語言特性智能交互。我不知道這一特性將會帶領咱們走向何方,但它蘊藏着無限的可能性,這令我感到異常興奮!

我何時能夠開始使用這一特性?

在服務器端,io.js支持ES6的模板字符串。

在瀏覽器端,Firefox 34+支持模板字符串。它們由去年夏天的實習生項目組裏的Guptha Rajagopal實現。模板字符串一樣在Chrome 41+中得以支持,可是IE和Safari都不支持。到目前爲止,若是你想要在web端使用模板字符串的功能,你將須要BabelTraceur協助你完成ES6到ES5的代碼轉譯,你也能夠在TypeScript中當即使用這一特性。

等等——那麼Markdown呢?

嗯?

哦…這是個好問題。

(這一章節與JavaScript無關,若是你不使用Markdown,能夠跳過這一章。)

對於模板字符串而言,Markdown和JavaScript如今都使用`字符來表示一些特殊的事物。事實上,在Markdown中,反撇號用來分割在內聯文本中間的代碼片斷。

這會帶來許多問題!若是你在Markdown中寫這樣的文檔:

To display a message, write `alert(`hello world!`)`.

它將這樣顯示:

To display a message, write alert(hello world!).

請注意,輸出文本中的反撇號消失了。Markdown將全部的四個反撇號解釋爲代碼分隔符並用HTML標籤將其替換掉。

爲了不這樣的狀況發生,咱們要藉助Markdown中的一個不爲人知的特性,你能夠使用多行反撇號做爲代碼分隔符,就像這樣:

To display a message, write ``alert(`hello world!`)``.

在這個Gist有具體代碼細節,它由Markdown寫成,因此你能夠直接查看源代碼。

不定參數和默認參數


不定參數

咱們一般使用可變參函數來構造API,可變參函數可接受任意數量的參數。例如,String.prototype.concat方法就能夠接受任意數量的字符串參數。ES6提供了一種編寫可變參函數的新方式——不定參數。

咱們經過一個簡單的可變參數函數containsAll給你們演示不定參數的用法。函數containsAll能夠檢查一個字符串中是否包含若干個子串,例如:containsAll("banana", "b", "nan")返回true,containsAll("banana", "c", "nan")返回false。

首先使用傳統方法來實現這個函數:

function containsAll(haystack) {
  for (var i = 1; i < arguments.length; i++) {
    var needle = arguments[i];
    if (haystack.indexOf(needle) === -1) {
      return false;
    }
  }
  return true;
}

在這個實現中,咱們用到了神奇的arguments對象,它是一個類數組對象,其中包含了傳遞給函數的全部參數。這段代碼實現了咱們的需求,但它的可讀性卻不是最理想的。函數的參數列表中只有一個參數 haystack,咱們沒法一眼就看出這個函數實際上接受了多個參數。另外,咱們必定要注意,應該從1開始迭代,而不是從0開始,由於 arguments[0]至關於參數haystack。若是咱們想要在haystack先後添加另外一個參數,咱們必定要記得更新循環體。不定參數剛好能夠解決可讀性與參數索引的問題。下面是用ES6不定參數特性實現的containsAll函數:

function containsAll(haystack, ...needles) {
  for (var needle of needles) {
    if (haystack.indexOf(needle) === -1) {
      return false;
    }
  }
  return true;
}

這一版containsAll函數與前者有相同的行爲,但這一版中使用了一個特殊的...needles語法。咱們來看一下調用 containsAll("banana", "b", "nan")以後的函數調用過程,與以前同樣,傳遞進來的第一個參數"banana"賦值給參數haystack,needles前的省略號代表它是一個不定參數,全部傳遞進來的其它參數都被放到一個數組中,賦值給變量needles。對於咱們的調用示例而言,needles被賦值爲["b", "nan"],後續的函數執行過程一如往常。(注意啦,咱們已經使用過ES6中for-of循環。)

在全部函數參數中,只有最後一個才能夠被標記爲不定參數。函數被調用時,不定參數前的全部參數都正常填充,任何「額外的」參數都被放進一個數組中並賦值給不定參數。若是沒有額外的參數,不定參數就是一個空數組,它永遠不會是undefined。

默認參數

一般來講,函數調用者不須要傳遞全部可能存在的參數,沒有被傳遞的參數可由感知到的默認參數進行填充。JavaScript有嚴格的默認參數格式,未被傳值的參數默認爲undefined。ES6引入了一種新方式,能夠指定任意參數的默認值。

下面是一個簡單的示例(反撇號表示模板字符串,上週已經討論過。):

function animalSentence(animals2="tigers", animals3="bears") {
    return `Lions and ${animals2} and ${animals3}! Oh my!`;
}

默認參數的定義形式爲[param1[ = defaultValue1 ][, ..., paramN[ = defaultValueN ]]],對於每一個參數而言,定義默認值時=後的部分是一個表達式,若是調用者沒有傳遞相應參數,將使用該表達式的值做爲參數默認值。相關示例以下:

 
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!

默認參數有幾個微妙的細節須要注意:

  • 默認值表達式在函數調用時自左向右求值,這一點與Python不一樣。這也意味着,默認表達式能夠使用該參數以前已經填充好的其它參數值。舉個例子,咱們優化一下剛剛那個動物語句函數:

function animalSentenceFancy(animals2="tigers",
    animals3=(animals2 == "bears") ? "sealions" : "bears")
{
  return `Lions and ${animals2} and ${animals3}! Oh my!`;
}

如今,animalSentenceFancy("bears")將返回「Lions and bears and sealions. Oh my!」。

  • 傳遞undefined值等效於不傳值,因此animalSentence(undefined, "unicorns")將返回「Lions and tigers and unicorns! Oh my!」。

  • 沒有默認值的參數隱式默認爲undefined,因此

function myFunc(a=42, b) {...}

是合法的,而且等效於

function myFunc(a=42, b=undefined) {...}

中止使用arguments

如今咱們已經看到了arguments對象可被不定參數和默認參數完美代替,移除arguments後一般會使代碼更易於閱讀。除了破壞可讀性外,衆所周知,針對arguments對象對JavaScript虛擬機進行的優化會致使一些讓你頭疼不已的問題。

咱們期待着不定參數和默認參數能夠徹底取代arguments,要實現這個目標,標準中增長了相應的限制:在使用不定參數或默認參數的函數中禁止使用arguments對象。曾經實現過arguments的引擎不會當即移除對它的支持,固然,如今更推薦使用不定參數和默認參數。

瀏覽器支持

Firefox早在第15版的時候就支持了不定參數和默認參數。

不幸的是,還沒有有其它已發佈的瀏覽器支持不定參數和默認參數。V8引擎最近增添了針對不定參數的實驗性的支持,而且有一個開放狀態的V8 issue給實現默認參數使用,JSC一樣也有一個開放的issue來給不定參數默認參數使用。

BabelTraceur編譯器都支持默認參數,因此從如今起就能夠開始使用。

解構 Destructuring


什麼是解構賦值?

解構賦值容許你使用相似數組或對象字面量的語法將數組和對象的屬性賦給各類變量。這種賦值語法極度簡潔,同時還比傳統的屬性訪問方法更爲清晰。

一般來講,你極可能這樣訪問數組中的前三個元素:

var first = someArray[0];
var second = someArray[1];
var third = someArray[2];

若是使用解構賦值的特性,將會使等效的代碼變得更加簡潔而且可讀性更高:

var [first, second, third] = someArray;

SpiderMonkey(Firefox的JavaScript引擎)已經支持解構的大部分功能,可是仍不健全。你能夠經過bug 694100跟蹤解構和其它ES6特性在SpiderMonkey中的支持狀況。

數組與迭代器的解構

以上是數組解構賦值的一個簡單示例,其語法的通常形式爲:

[ variable1, variable2, ..., variableN ] = array;

這將爲variable1到variableN的變量賦予數組中相應元素項的值。若是你想在賦值的同時聲明變量,可在賦值語句前加入varletconst關鍵字,例如:

var [ variable1, variable2, ..., variableN ] = array;
let [ variable1, variable2, ..., variableN ] = array;
const [ variable1, variable2, ..., variableN ] = array;

事實上,用變量來描述並不恰當,由於你能夠對任意深度的嵌套數組進行解構:

var [foo, [[bar], baz]] = [1, [[2], 3]];
console.log(foo);
// 1
console.log(bar);
// 2
console.log(baz);
// 3

此外,你能夠在對應位留空來跳過被解構數組中的某些元素:

var [,,third] = ["foo", "bar", "baz"];
console.log(third);
// "baz"

並且你還能夠經過「不定參數」模式捕獲數組中的全部尾隨元素:

var [head, ...tail] = [1, 2, 3, 4];
console.log(tail);
// [2, 3, 4]

當訪問空數組或越界訪問數組時,對其解構與對其索引的行爲一致,最終獲得的結果都是:undefined

console.log([][0]);
// undefined
var [missing] = [];
console.log(missing);
// undefined

請注意,數組解構賦值的模式一樣適用於任意迭代器:

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

對象的解構

經過解構對象,你能夠把它的每一個屬性與不一樣的變量綁定,首先指定被綁定的屬性,而後緊跟一個要解構的變量。

var robotA = { name: "Bender" };
var robotB = { name: "Flexo" };
var { name: nameA } = robotA;
var { name: nameB } = robotB;
console.log(nameA);
// "Bender"
console.log(nameB);
// "Flexo"

當屬性名與變量名一致時,能夠經過一種實用的句法簡寫:

var { foo, bar } = { foo: "lorem", bar: "ipsum" };
console.log(foo);
// "lorem"
console.log(bar);
// "ipsum"

與數組解構同樣,你能夠隨意嵌套並進一步組合對象解構:

var complicatedObj = {
  arrayProp: [
    "Zapp",
    { second: "Brannigan" }
  ]
};
var { arrayProp: [first, { second }] } = complicatedObj;
console.log(first);
// "Zapp"
console.log(second);
// "Brannigan"

當你解構一個未定義的屬性時,獲得的值爲undefined

var { missing } = {};
console.log(missing);
// undefined

請注意,當你解構對象並賦值給變量時,若是你已經聲明或不打算聲明這些變量(亦即賦值語句前沒有letconstvar關鍵字),你應該注意這樣一個潛在的語法錯誤:

{ blowUp } = { blowUp: 10 };
// Syntax error 語法錯誤

爲何會出錯?這是由於JavaScript語法通知解析引擎將任何以{開始的語句解析爲一個塊語句(例如,{console}是一個合法塊語句)。解決方案是將整個表達式用一對小括號包裹:

({ safe } = {});
// No errors 沒有語法錯誤

解構值不是對象、數組或迭代器

當你嘗試解構nullundefined時,你會獲得一個類型錯誤:

var {blowUp} = null;
// TypeError: null has no properties(null沒有屬性)

然而,你能夠解構其它原始類型,例如:布爾值數值字符串,可是你將獲得undefined

var {wtf} = NaN;
console.log(wtf);
// undefined

你可能對此感到意外,但通過進一步審查你就會發現,緣由其實很是簡單。當使用對象賦值模式時,被解構的值須要被強制轉換爲對象。大多數類型均可以被轉換爲對象,但nullundefined卻沒法進行轉換。當使用數組賦值模式時,被解構的值必定要包含一個迭代器

默認值

當你要解構的屬性未定義時你能夠提供一個默認值:

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目前只實現了這個特性的前兩種狀況,第三種還沒有實現。詳情查看bug 932080。)

解構的實際應用

函數參數定義

做 爲開發者,咱們須要實現設計良好的API,一般的作法是爲函數爲函數設計一個對象做爲參數,而後將不一樣的實際參數做爲對象屬性,以免讓API使用者記住 多個參數的使用順序。咱們能夠使用解構特性來避免這種問題,當咱們想要引用它的其中一個屬性時,大可沒必要反覆使用這種單一參數對象。

function removeBreakpoint({ url, line, column }) {
  // ...
}

這是一段來自Firefox開發工具JavaScript調試器(一樣使用JavaScript實現——沒錯,就是這樣!)的代碼片斷,它看起來很是簡潔,咱們會發現這種代碼模式特別討喜。

配置對象參數

延伸一下以前的示例,咱們一樣能夠給須要解構的對象屬性賦予默認值。當咱們構造一個提供配置的對象,而且須要這個對象的屬性攜帶默認值時,解構特性就派上用場了。舉個例子,jQuery的ajax函數使用一個配置對象做爲它的第二參數,咱們能夠這樣重寫函數定義:

jQuery.ajax = function (url, {
  async = true,
  beforeSend = noop,
  cache = true,
  complete = noop,
  crossDomain = false,
  global = true,
  // ... 更多配置
}) {
  // ... do stuff
};

如此一來,咱們能夠避免對配置對象的每一個屬性都重複var foo = config.foo || theDefaultFoo;這樣的操做。

(編者按:不幸的是,對象的默認值簡寫語法仍未在Firefox中實現,我知道,上一個編者按後的幾個段落講解的就是這個特性。點擊bug 932080查看最新詳情。)

與ES6迭代器協議協同使用

ECMAScript 6中定義了一個迭代器協議,咱們在《深刻淺出ES6(二):迭代器和for-of循環》中已經詳細解析過。當你迭代Maps(ES6標準庫中新加入的一種對象)後,你能夠獲得一系列形如[key, value]的鍵值對,咱們可將這些鍵值對解構,更輕鬆地訪問鍵和值:

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語言中還沒有整合多重返回值的特性,可是無須畫蛇添足,由於你本身就能夠返回一個數組並將結果解構:

function returnMultipleValues() {
  return [1, 2];
}
var [foo, bar] = returnMultipleValues();

或者,你能夠用一個對象做爲容器併爲返回值命名:

function returnMultipleValues() {
  return {
    foo: 1,
    bar: 2
  };
}
var { foo, bar } = returnMultipleValues();

這兩個模式都比額外保存一個臨時變量要好得多。

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) => ...);
使用解構導入部分CommonJS模塊

你是否還沒有使用ES6模塊?還用着CommonJS的模塊呢吧!沒問題,當咱們導入CommonJS模塊X時,極可能在模塊X中導出了許多你根本沒打算用的函數。經過解構,你能夠顯式定義模塊的一部分來拆分使用,同時還不會污染你的命名空間:

const { SourceMapConsumer, SourceNode } = require("source-map");

(若是你使用ES6模塊,你必定知道在import聲明中有一個類似的語法。)

正如你所見,解構在許多獨立小場景中很是實用。在Mozilla咱們已經積累了許多有關解構的使用經驗。十年前,Lars Hansen在Opera中引入了JS解構特性,Brendan Eich隨後就給Firefox也增長了相應的支持,移植時版本爲Firefox 2。因此咱們能夠確定,漸漸地,你會在天天使用的語言中加入解構這個新特性,它可讓你的代碼變得更加精簡整潔。

箭頭函數 Arrow Functions


箭頭符號在JavaScript誕生時就已經存在,當初第一個JavaScript教程曾建議在HTML註釋內包裹行內腳本,這樣能夠避免不支持JS的瀏覽器誤將JS代碼顯示爲文本。你會寫這樣的代碼:

<script language="javascript">
<!--
  document.bgColor = "brown";  // red
// -->
</script>

老式瀏覽器會將這段代碼解析爲兩個不支持的標籤和一條註釋,只有新式瀏覽器才能識別出其中的JS代碼。

爲了支持這種奇怪的hack方式,瀏覽器中的JavaScript引擎將<!-- 這四個字符解析爲單行註釋的起始部分,我沒開玩笑,這自始至終就是語言的一部分,直到如今仍然有效,這種註釋符號不只出現<script>標籤後的首行,在JS代碼的每一個角落你都有可能見到它,甚至在Node中也是如此。

碰巧,這種註釋風格首次在ES6中被標準化了,但在新標準中箭頭被用來作其它事情。

箭頭序列 —> 一樣是單行註釋的一部分。古怪的是,在HTML中-->以前的字符是註釋的一部分,而在JS中-->以後的部分纔是註釋。

你必定感到陌生的是,只有當箭頭在行首時纔會註釋當前行。這是由於在其它上下文中,-->是一個JS運算符:「趨向於」運算符!

function countdown(n) {
  while (n --> 0)  // "n goes to zero"
    alert(n);
  blastoff();
}

上面這段代碼能夠正常運行,循環會一直重複直到n趨於0,這固然不是ES6中的新特性,它只不過是將兩個你早已熟悉的特性經過一些誤導性的手段結合在一塊兒。你能理解麼?一般來講,相似這種謎團均可以在Stack Overflow上找到答案。

固然,一樣地,小於等於操做符<=也形似箭頭,你能夠在JS代碼、隱藏的圖片樣式中找到更多相似的箭頭,可是咱們就不繼續尋找了,你應該注意到咱們漏掉了一種特殊的箭頭。

<!--
單行註釋
 
-->
「趨向於」操做符
 
<=
小於等於
 
=>
這又是什麼?

=>究竟是什麼?咱們今天就來一探究竟。

首先,咱們談論一些有關函數的事情。

函數表達式無處不在

JavaScript中有一個有趣的特性,不管什麼時候,當你須要一個函數時,你均可以在想添加的地方輸入這個函數。

舉個例子,假設你嘗試告訴瀏覽器用戶點擊一個特定按鈕後的行爲,你會這樣寫:

$("#confetti-btn").click(

jQuery的.click()方法接受一個參數:一個函數。沒問題,你能夠在這裏輸入一個函數:

$("#confetti-btn").click(function (event) {
  playTrumpet();
  fireConfettiCannon();
});

對於如今的咱們來講,寫出這樣的代碼至關天然,而回憶起在這種編程方式流行以前,這種寫法相對陌生一些,許多語言中都沒有這種特性。1958年,Lisp首先支持函數表達式,也支持調用lambda函數,而C++,Python、C#以及Java在隨後的多年中一直不支持這樣的特性。

如今大相徑庭,全部的四種語言都已支持lambda函數,更新出現的語言廣泛都支持內建的lambda函數。咱們必需要感謝JavaScript和早期的JavaScript程序員,他們勇敢地構建了重度依賴lambda函數的庫,讓這種特性被普遍接受。

使人傷感的是,隨後在全部我說起的語言中,只有JavaScript的lambda的語法最終變得冗長乏味。

// 六種語言中的簡單函數示例
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中引入了一種編寫函數的新語法

// ES5
var selected = allJobs.filter(function (job) {
  return job.isSelected();
});
// ES6
var selected = allJobs.filter(job => job.isSelected());

當你只須要一個只有一個參數的簡單函數時,能夠使用新標準中的箭頭函數,它的語法很是簡單:標識符=>表達式。你無需輸入functionreturn,一些小括號、大括號以及分號也能夠省略。

(我我的對於這個特性很是感激,再也不須要輸入function這幾個字符對我而言相當重要,由於我老是不可避免地錯誤寫成functoin,而後我就不得不回過頭改正它。)

若是要寫一個接受多重參數(也可能沒有參數,或者是不定參數、默認參數參數解構)的函數,你須要用小括號包裹參數list。

// ES5
var total = values.reduce(function (a, b) {
  return a + b;
}, 0);
// ES6
var total = values.reduce((a, b) => a + b, 0);

我認爲這看起來酷斃了。

正如你使用相似Underscore.jsImmutable.js這樣的庫提供的函數工具,箭頭函數運行起來一樣美不可言。事實上,Immutable的文檔中的示例全都由ES6寫成,其中的許多特性已經用上了箭頭函數。

那麼不是很是函數化的狀況又如何呢?除表達式外,箭頭函數還能夠包含一個塊語句。回想一下咱們以前的示例:

// ES5
$("#confetti-btn").click(function (event) {
  playTrumpet();
  fireConfettiCannon();
});

這是它們在ES6中看起來的樣子:

// ES6
$("#confetti-btn").click(event => {
  playTrumpet();
  fireConfettiCannon();
});

這是一個微小的改進,對於使用了Promises的代碼來講箭頭函數的效果能夠變得更加戲劇性,}).then(function (result) { 這樣的一行代碼能夠堆積起來。

注意,使用了塊語句的箭頭函數不會自動返回值,你須要使用return語句將所需值返回。

小提示:當使用箭頭函數建立普通對象時,你老是須要將對象包裹在小括號裏。

// 爲與你玩耍的每個小狗建立一個新的空對象
var chewToys = puppies.map(puppy => {});   // 這樣寫會報Bug!
var chewToys = puppies.map(puppy => ({})); //

用小括號包裹空對象就能夠了。

不幸的是,一個空對象{}和一個空的塊{}看起來徹底同樣。ES6中的規則是,緊隨箭頭的{被解析爲塊的開始,而不是對象的開始。所以,puppy => {}這段代碼就被解析爲沒有任何行爲並返回undefined的箭頭函數。

更使人困惑的是,你的JavaScript引擎會將相似{key: value}的對象字面量解析爲一個包含標記語句的塊。幸運的是,{是惟一一個有歧義的字符,因此用小括號包裹對象字面量是惟一一個你須要牢記的小竅門。

這個函數的this值是什麼呢?

普通function函數和箭頭函數的行爲有一個微妙的區別,箭頭函數沒有它本身的this,箭頭函數內的this值繼承自外圍做用域。

在咱們嘗試說明這個問題前,先一塊兒回顧一下。

JavaScript中的this是如何工做的?它的值從哪裏獲取?這些問題的答案可都不簡單,若是你對此倍感清晰,必定由於你長時間以來一直在處理相似的問題。

這個問題常常出現的其中一個緣由是,不管是否須要,function函數總會自動接收一個this值。你是否寫過這樣的hack代碼:

{
  ...
  addAll: function addAll(pieces) {
    var self = this;
    _.each(pieces, function (piece) {
      self.add(piece);
    });
  },
  ...
}

在這裏,你但願在內層函數裏寫的是this.add(piece),不幸的是,內層函數並未從外層函數繼承this的值。在內層函數裏,this會是windowundefined,臨時變量self用來將外部的this值導入內部函數。(另外一種方式是在內部函數上執行.bind(this),兩種方法都不甚美觀。)

在ES6中,不須要再hackthis了,但你須要遵循如下規則:

  • 經過object.method()語法調用的方法使用非箭頭函數定義,這些函數須要從調用者的做用域中獲取一個有意義的this值。
  • 其它狀況全都使用箭頭函數。
// ES6
{
  ...
  addAll: function addAll(pieces) {
    _.each(pieces, piece => this.add(piece));
  },
  ...
}

在ES6的版本中,注意addAll方法從它的調用者處獲取了this值,內部函數是一個箭頭函數,因此它繼承了外圍做用域的this值。

超讚的是,在ES6中你能夠用更簡潔的方式編寫對象字面量中的方法,因此上面這段代碼能夠簡化成:

// ES6的方法語法
{
  ...
  addAll(pieces) {
    _.each(pieces, piece => this.add(piece));
  },
  ...
}

在方法和箭頭函數之間,我不再會錯寫functoin了,這真是一個絕妙的設計思想!

箭頭函數與非箭頭函數間還有一個細微的區別,箭頭函數不會獲取它們本身的arguments對象。誠然,在ES6中,你可能更多地會使用不定參數和默認參數值這些新特性。

藉助箭頭函數洞悉計算機科學的風塵往事

咱們已經討論了許多箭頭函數的實際用例,它還有一種可能的使用方法:將ES6箭頭函數做爲一個學習工具,來深刻挖掘計算的本質,是否實用,終將取決於你本身。

1936年,Alonzo Church和Alan Turing各自開發了強大的計算數學模型,圖靈將他的模型稱爲a-machines,可是每個人都稱其爲圖靈機。Church寫的是函數模型,他的模型被稱爲lambda演算λ-calculus)。這一成果也被Lisp借鑑,用LAMBDA來指示函數,這也是爲什麼咱們如今將函數表達式稱爲lambda函數。

但什麼是Lambda演算呢?「計算模型」又意味着什麼呢?

用幾句話解釋清楚很難,可是我會努力闡釋:lambda演算是第一代編程語言的一種形式,但畢竟存儲程序計算機在十幾二十年後才誕生,因此它本來不是爲編程語言設計的,而是爲了表達任意你想到的計算問題設計的一種極度簡化的純數學思想的語言。Church但願用這個模型來證實廣泛意義的計算。

最終他發現,在他的系統中只須要一件東西:函數。

這種聲明方式無與倫比,不借助對象、數組、數字、if語句、while循環、分號、賦值、邏輯運算符甚或是事件循環,只須使用函數就能夠從0開始重建JavaScript能實現的每一種計算。

這是用Church的lambda標記寫出來的數學家風格的「程序」示例:

fix = λf.(λx.f(λv.x(x)(v)))(λx.f(λv.x(x)(v)))

等效的JavaScript函數是這樣的:

var fix = f => (x => f(v => x(x)(v)))
               (x => f(v => x(x)(v)));

因此,在JavaScript中實現了一個能夠運行的lambda演算,它根植於這門語言中。

Alonzo Church和lambda演算後繼研究者們的故事,以及它是如何潛移默化地入駐每一門主流編程語言的,已經遠超本文的討論範圍。可是若是你對計算機科學 的奠定感興趣,或者你只是對一門只用函數就能夠作許多相似循環和遞歸這樣的事情的語言倍感興趣,你能夠在一個下雨的午後深刻邱奇數Church numerals)和不動點組合子Fixed-point combinator),在你的Firefox控制檯或Scratchpad中仔細研究一番。結合ES6的箭頭函數以及其它強大的功能,JavaScript稱得上是一門探索lambda演算的最好的語言。

我什麼時候能夠使用箭頭函數?

早在2013年,我就在Firefox中實現了ES6箭頭函數的功能,Jan de Mooij爲其優化加快了執行速度。感謝Tooru Fujisawa以及ziyunfei(譯者注:中國開發者,爲Mozilla做了許多貢獻)後續打的補丁。

微軟Edge預覽版中也實現了箭頭函數的功能,若是你想當即在你的Web項目中使用箭頭函數,能夠使用BabelTraceurTypeScript,這三個工具均已實現相關功能。

Symbols


你是否知道ES6中的Symbols是什麼,它有什麼做用呢?我相信你極可能不知道,那就讓咱們一探究竟!

Symbols並不是用來指代某種Logo。

它們也不是能夠用做代碼的小圖標。

它們不是代替其它東西的文學手法。

它們更不可能被用來指代諧音詞Cymbals(鐃鈸)。

(編程的時候最好不要演奏鐃鈸,它們太過吵鬧,極可能致使你的程序崩潰。)

那麼,Symbols究竟是什麼呢?

它是JavaScript的第七種原始類型

1997年JavaScript首次被標準化,那時只有六種原始類型,在ES6之前,JS程序中使用的每個值都是如下幾種類型之一:

  • Undefined 未定義
  • Null 空值
  • Boolean 布爾類型
  • Number 數字類型
  • String 字符串類型
  • Object 對象類型

每種類型都是多個值的集合,前五個集合是有限的。布爾類型只有兩個值,truefalse,不會再創造第三種布爾值;數字類型和字符串類型的值更多,標準指明一共有18,437,736,874,454,810,627種不一樣的數字(包括NaN, 亦即「Not a Number」的縮寫,表明非數字),可能存在的字符串類型的值擁有無以匹敵的數量,我估算了一下大約是 (2144,115,188,075,855,872 − 1) ÷ 65,535種……固然,我極可能得出了一個錯誤的答案,但字符串類型值的集合必定是有限的。

然而,對象類型值的集合是無限的。每個對象都像珍貴的雪花同樣獨一無二,每一次你打開一個Web頁面,都會建立一堆對象。

ES6新特性中的symbol也是值,但它不是字符串,也不是對象,而是是全新的——第七種類型的原始值。

讓咱們一塊兒探討一下symbol的實際應用場景。

從一個簡單的布爾類型出發

有時候你能夠很是輕鬆地將別人的外部數據存儲到一個JavaScript對象中。

舉 個例子,假設你正在寫一個JS庫,能夠經過CSS transitions使DOM元素在屏幕上移動。你可能會注意到,當你嘗試在一個div元素上同時應用多重CSS transitions時並不會生效。實際效果是醜陋而又不連續的「跳閃」。你認爲能夠修復這個問題,但前提是你須要一種發現給定元素是否已經移動過的方 法。

應當如何解決這個問題呢?

一種方法是,用CSS API來告訴瀏覽器元素是否正在移動,但這樣簡直小題大作。在元素移動的第一時間內你的庫就應該記錄下移動的狀態,因此它天然知道元素正在移動。

你真正想要的是一種持續跟蹤某個元素正在移動的方法。你能夠維護一個數組,記錄全部正在移動的元素,每當你的庫被調用來移動某個元素時,你能夠檢索數組來查看元素是否已經存在,亦即它是否正在移動中。

固然,若是數組很是大的話,線性搜索將會很是緩慢。

實際上你只想爲元素設置一個標記:

if (element.isMoving) {
  smoothAnimations(element);
}
element.isMoving = true;

這樣也會有一些潛在的問題,事實上,你的代碼極可能不是惟一一段操做DOM的代碼。

  1. 你建立的屬性極可能影響到其它使用了for-inObject.keys()的代碼。
  2. 一些聰明的庫做者可能已經考慮並使用了這項技術,這樣一來你的庫就會與已有的庫產生某些衝突
  3. 固然,極可能你比他們更聰明,你先採用了這項技術,可是他們的庫仍然沒法與你的庫默契配合。
  4. 標準委員會可能決定爲全部的元素增長一個.isMoving()方法,到那時你須要重寫相關邏輯,一定會有深深的挫敗感。

固然你能夠選擇一個乏味而愚蠢的命名(其餘人根本不會想用的那些名稱)來解決最後的三個問題:

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;

這隻會形成無畏的眼疲勞。

藉助於密碼學,你能夠生成一個惟一的屬性名稱:

// 獲取1024個Unicode字符的無心義命名
var isMoving = SecureRandom.generateName();
...
if (element[isMoving]) {
  smoothAnimations(element);
}
element[isMoving] = true;

object[name]語法容許你使用幾乎任何字符串做爲屬性名稱。因此這個方法行之有效:衝突幾乎是不可能的,而且你的代碼看起來也很簡潔。

可是這也將帶來不良的調試體驗。每當你在控制檯輸出(console.log())包含那個屬性的元素時,你將會看到一堆巨大的字符串垃圾。假使你須要比這多得多的相似屬性呢?你如何保持它們整齊劃一?每當你重載的時候它們的命名甚至都不同!

爲何這個問題如此困難?咱們只想要一個小小的布爾值啊!

symbol是最終的解決方案

symbol是程序建立而且能夠用做屬性鍵的值,而且它能避免命名衝突的風險。

var mySymbol = Symbol();

調用Symbol()建立一個新的symbol,它的值與其它任何值皆不相等。

字符串或數字能夠做爲屬性的鍵,symbol也能夠,它不等同於任何字符串,於是這個以symbol爲鍵的屬性能夠保證不與任何其它屬性產生衝突。

obj[mySymbol] = "ok!";  // 保證不會衝突
console.log(obj[mySymbol]);  // ok!

想要在上述討論的場景中使用symbol,你能夠這樣作:

// 建立一個獨一無二的symbol
var isMoving = Symbol("isMoving");
...
if (element[isMoving]) {
  smoothAnimations(element);
}
element[isMoving] = true;

有關這段代碼的一些解釋:

  • Symbol("isMoving")中的isMoving被稱做描述。你能夠經過console.log()將它打印出來,對調試很是有幫助;你也能夠用.toString()方法將它轉換爲字符串呈現;它也能夠被用在錯誤信息中。

  • element[isMoving]被稱做一個以symbol爲鍵(symbol-keyed)的屬性。簡而言之,它的名字是symbol而不是一個字符串。除此以外,它與一個普通的屬性沒有什麼區別。

  • 以symbol爲鍵的屬性屬性與數組元素相似,不能被相似obj.name的點號法訪問,你必須使用方括號訪問這些屬性。

  • 若是你已經獲得了symbol,那麼訪問一個以symbol爲鍵的屬性一樣簡單,以上的示例很好地展現瞭如何獲取element[isMoving]的值以及如何爲它賦值。若是咱們須要,能夠查看屬性是否存在:if (isMoving in element),也能夠刪除屬性:delete element[isMoving]

  • 另外一方面,只有當isMoving在當前做用域中時纔會生效。這是symbol的弱封裝機制:模塊建立了幾個symbol,能夠在任意對象上使用,無須擔憂與其它代碼建立的屬性產生衝突。

symbol鍵的設計初衷是避免初衷,所以JavaScript中最多見的對象檢查的特性會忽略symbol鍵。例如,for-in循環只會遍歷對象的字符串鍵,symbol鍵直接跳過,Object.keys(obj)Object.getOwnPropertyNames(obj)也是同樣。可是symbols也不徹底是私有的:用新的API Object.getOwnPropertySymbols(obj)就能夠列出對象的symbol鍵。另外一個新的API,Reflect.ownKeys(obj),會同時返回字符串鍵和symbol鍵。(咱們將在隨後的文章中講解Reflect(反射) API)。

慢慢地咱們會發現,愈來愈多的庫和框架將大量使用symbol,語言自己也會將symbol應用於普遍的用途。

可是,到底什麼是symbol呢?

> typeof Symbol()
"symbol"

確切地說,symbol與其它類型並不徹底相像。

symbol被建立後就不可變動,你不能爲它設置屬性(在嚴格模式下嘗試設置屬性會獲得TypeError的錯誤)。他們能夠用做屬性名稱,這些性質與字符串相似。

另外一方面,每個symbol都獨一無二,不與其它symbol等同,即便兩者有相同的描述也不相等;你能夠輕鬆地建立一個新的symbol。這些性質與對象相似。

ES6中的symbol與Lisp和Ruby這些語言中更傳統的symbol相似,但不像它們集成得那麼緊密。在Lisp中,全部的標識符都是symbol;在JS中,標識符和大多數的屬性鍵仍然是字符串,symbol只是一個額外的選項。

關於symbol的忠告:symbol不能被自動轉換爲字符串,這和語言中的其它類型不一樣。嘗試拼接symbol與字符串將獲得TypeError錯誤。

> 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

經過String(sym)sym.toString()能夠顯示地將symbol轉換爲一個字符串,從而回避這個問題。

獲取symbol的三種方法

有三種獲取symbol的方法。

  • 調用Symbol()。正如咱們上文中所討論的,這種方式每次調用都會返回一個新的惟一symbol。

  • 調用Symbol.for(string)。這種方式會訪問symbol註冊表,其中存儲了已經存在的一系列symbol。這種方式與經過Symbol()定義的獨立symbol不一樣,symbol註冊表中的symbol是共享的。若是你連續三十次調用Symbol.for("cat"),每次都會返回相同的symbol。註冊表很是有用,在多個web頁面或同一個web頁面的多個模塊中常常須要共享一個symbol。

  • 使用標準定義的symbol,例如:Symbol.iterator。標準根據一些特殊用途定義了少量的幾個symbol。

若是你尚不肯定symbol是否實用,最後這一章將向你展現symbol在實際應用中發揮的巨大做用,很是有趣!

symbol在ES6規範中的應用

在以前的文章《深刻淺出ES6(二):迭代器和for-of循環》中,咱們已經領略了藉助ES6 symbol的力量避免代碼衝突的方法,循環for (var item of myArray)首先調用myArray[Symbol.iterator](),當時我提到這種寫法是爲了替代myArray.iterator(),擁有更好的向後兼容性。

如今咱們知道symbol究竟是什麼了,天然很容易理解爲何咱們要創造一個symbol以及它爲咱們帶來什麼新特性。

ES6中還有其它幾處使用了symbol的地方。(這些特性在Firefox裏還沒有實現。)

  • 使instanceof可擴展。在ES6中,表達式object instanceof constructor被指定爲構造函數的一個方法:constructor[Symbol.hasInstance](object)。這意味着它是可擴展的。

  • 消除新特性和舊代碼之間的衝突。這一點很是複雜,可是咱們發現,添加某些ES6數組方法會破壞現有的Web網站。其它Web標準有相同的問題:向瀏覽器中添加新方法會破壞原有的網站。然而,破壞問題主要由動態做用域引發,因此ES6引入一個特殊的symbol——Symbol.unscopables,Web標準能夠用這個symbol來阻止某些方法別加入到動態做用域中。

  • 支持新的字符串匹配類型。在ES5中,str.match(myObject)會嘗試將myObject轉換爲正則表達式對象(RegExp)。在ES6中,它會首先檢查myObject是否有一個myObject[Symbol.match](str)方法。如今的庫能夠提供自定義的字符串解析類,全部支持RegExp對象的環境均可以正常運行。

這些用例的應用範圍都很是小,很難看到這些特性經過它們自身影響咱們每日的代碼,長期來看才能體現它們的價值。實際上,symbol是PHP和Python中的__doubleUnderscores在JavaScript語言環境中的改進版。標準將藉助symbol的力量在將來向語言中添加新的鉤子,同時無風險地將新特性添加到你已有的代碼中。

我什麼時候能夠使用ES6 symbol?

symbol在Firefox 36和Chrome 38中均已被實現。Firefox中的實現由我親自完成,因此若是你的symbol像鐃鈸(cymbals)同樣行爲異常,請直接聯繫我!

爲了支持那些還沒有支持原生ES6 symbol的瀏覽器,你能夠使用一個polyfill,例如core.js。由於symbol與其它類型不盡相同,因此polyfill目前不是很完美。請閱讀注意事項

集合


前段時間,官方名爲「ECMA-262,第六版,ECMAScript 2015語言規範」的ES6規範終於結束了最後的征途,正式被承認爲新的ECMA標準。讓咱們祝賀TC39等全部做出貢獻人們,ES6終於定稿了!

更好的消息是,下次更新不須要再等六年了。委員會如今努力要求,大約每12個月完成一個新的版本。第七版提議已經開始。

如今是時候慶祝慶祝了,讓咱們來討論一些好久以來我一直但願在JS裏看到的東西——固然,它們之後仍然有改進的餘地。

共同發展中的難題

JS和其它編程語言有些特殊的差異,有時,它們會以使人驚奇的方式影響到這門語言的發展。

ES6模塊就是個很好的例子。其它語言的模塊化系統中,Racket作得特別棒,Python也很好。那麼,當標準委員會決定在ES6中增長模塊時,爲何他們不直接仿照一套已經存在的系統呢?

由於JS是不一樣的,由於它要在瀏覽器裏運行。讀取和寫入均可能花費較長時間,因此,JS須要一套支持異步加載代碼的模塊化系統,同時,也不能容許在文件夾中挨個搜索,照搬已有的系統並不能解決問題。ES6的模塊化系統須要一些新技術。

討論這些問題對最終設計的影響,會是個有趣的故事,不過咱們今天要討論的並非模塊。

這篇文章是關於ES6標準中所謂「鍵值集合」的:SetMapWeakSetWeakMap。它們在大多數方面和其它語言中的哈希表同樣,不過,正由於JS是不一樣的,標準委員會在其中作了些有趣的權衡與調整。

爲何要集合?

熟悉JS必定會知道,咱們已經有了一種相似哈希表的東西:對象(Object)。

一個普通的對象畢竟就只是一個開放的鍵值對集合。你能夠進行獲取、設置、刪除、遍歷——任何一個哈希表支持的操做。因此咱們到底爲何要增長新的特性?

好吧,大多數程序簡單地用對象來存儲鍵值對就夠了,對它們而言,沒什麼必要換用MapSet。可是,直接這樣使用對象有一些廣爲人知的問題:

  • 做爲查詢表使用的對象,不能既支持方法又保證避免衝突。
  • 於是,要麼得用Object.create(null)而非直接寫{},要麼得當心地避免把Object.prototype.toString之類的內置方法名做爲鍵名來存儲數據。
  • 對象的鍵名老是字符串(固然,ES6 中也能夠是Symbol)而不能是另外一個對象。
  • 沒有有效的獲知屬性個數的方法。

ES6中又出現了新問題:純粹的對象不可遍歷,也就是,它們不能配合for-of循環或...操做符等語法。

嗯,確實不少程序裏這些問題都不重要,直接用純對象仍然是正確的選擇。MapSet是爲其它場合準備的。

這些ES6中的集合原本就是爲避免用戶數據與內置方法衝突而設計的,因此它們不會把數據做爲屬性暴露出來。也就是說,obj.keyobj[key]不能再用來訪問數據了,取而代之的是map.get(key)。同時,不像屬性,哈希表的鍵值不能經過原型鏈來繼承了。

好消息是,不像純粹的ObjectMapSet有本身的方法了,而且,更多標準或自定義的方法能夠無需擔憂衝突地加入。

Set

一個Set是一羣值的集合。它是可變的,可以增刪元素。如今,還沒說到它和數組的區別,不過它們的區別就和類似點同樣多。

首先,和數組不一樣,一個Set不會包含相同元素。試圖再次加入一個已有元素不會產生任何效果。

這個例子裏元素都是字符串,不過Set是能夠包含JS中任何類型的值的。一樣,重複加入已有元素不會產生效果。

其次,Set的數據存儲結構專門爲一種操做做了速度優化:包含性檢測。

> // 檢查"zythum"是否是一個單詞
> arrayOfWords.indexOf("zythum") !== -1  // 慢
    true
> setOfWords.has("zythum")               // 快
    true

Set不能提供的則是索引。

> arrayOfWords[15000]
    "anapanapa"
> setOfWords[15000]   // Set不支持索引
    undefined

如下是Set支持的全部操做:

  • new Set:建立一個新的、空的Set
  • new Set(iterable):從任何可遍歷數據中提取元素,構造出一個新的集合。
  • set.size:獲取集合的大小,即其中元素的個數。
  • set.has(value):斷定集合中是否含有指定元素,返回一個布爾值。
  • set.add(value):添加元素。若是與已有重複,則不產生效果。
  • set.delete(value):刪除元素。若是並不存在,則不產生效果。.add().delete()都會返回集合自身,因此咱們能夠用鏈式語法。
  • set[Symbol.iterator]():返回一個新的遍歷整個集合的迭代器。通常這個方法不會被直接調用,由於實際上就是它使集合可以被遍歷,也就是說,咱們能夠直接寫for (v of set) {...}等等。
  • set.forEach(f):直接用代碼來解釋好了,它就像是for (let value of set) { f(value, value, set); }的簡寫,相似於數組的.forEach()方法。
  • set.clear():清空集合。
  • set.keys()set.values()set.entries()返回各類迭代器,它們是爲了兼容Map而提供的,因此咱們待會兒再來看。

在這些特性中,負責構造集合的new Set(iterable)是惟一一個在整個數據結構層面上操做的。你能夠用它把數組轉化爲集合,在一行代碼內去重;也能夠傳遞一個生成器,函數會逐個遍歷它,並把生成的值收錄爲一個集合;也能夠用來複制一個已有的集合。

上週我答應過要給ES6中的新集合們挑挑刺,就從這裏開始吧。儘管Set已經很不錯了,仍是有些被遺漏的方法,說不定補充到未來某個標準裏會挺不錯:

  • 目前數組已經有的一些輔助函數,好比.map().filter().some().every()
  • 不改變原值的交併操做,好比set1.union(set2)set1.intersection(set2)
  • 批量操做,如set.addAll(iterable)set.removeAll(iterable)set.hasAll(iterable)

好消息是,這些均可以用ES6已經提供了的方法來實現。

Map

一個Map對象由若干鍵值對組成,支持:

  • new Map:返回一個新的、空的Map
  • new Map(pairs):根據所含元素形如[key, value]的數組pairs來建立一個新的Map。這裏提供的pairs能夠是一個已有的Map 對象,能夠是一個由二元數組組成的數組,也能夠是逐個生成二元數組的一個生成器,等等。
  • map.size:返回Map中項目的個數。
  • map.has(key):測試一個鍵名是否存在,相似key in obj
  • map.get(key):返回一個鍵名對應的值,若鍵名不存在則返回undefined,相似obj[key]
  • map.set(key, value):添加一對新的鍵值對,若是鍵名已存在就覆蓋。
  • map.delete(key):按鍵名刪除一項,相似delete obj[key]
  • map.clear():清空Map
  • map[Symbol.iterator]():返回遍歷全部項的迭代器,每項用一個鍵和值組成的二元數組表示。
  • map.forEach(f) 相似for (let [key, value] of map) { f(value, key, map); }。這裏詭異的參數順序,和Set中同樣,是對應着Array.prototype.forEach()
  • map.keys():返回遍歷全部鍵的迭代器。
  • map.values():返回遍歷全部值的迭代器。
  • map.entries():返回遍歷全部項的迭代器,就像map[Symbol.iterator]()。實際上,它們就是同一個方法,不一樣名字。

還有什麼要抱怨的?如下是我以爲會有用而ES6還沒提供的特性:

  • 鍵不存在時返回的默認值,相似 Python 中的collections.defaultdict
  • 一個能夠叫Map.fromObject(obj)的輔助函數,以便更方便地用構造對象的語法來寫出一個Map

一樣,這些特性也是很容易加上的。

到這裏,還記不記得,開篇時我提到過運行於瀏覽器對語言特性設計的特殊影響?如今要好好談一談這個問題了。我已經有了三個例子,如下是前兩個。

JS是不一樣的,第一部分:沒有哈希代碼的哈希表?

到目前爲止,據我所知,ES6的集合類徹底不支持下述這種有用的特性。

好比說,咱們有若干 URL 對象組成的Set:

var urls = new Set;
urls.add(new URL(location.href));  // 兩個 URL 對象。
urls.add(new URL(location.href));  // 它們同樣麼?
alert(urls.size);  // 2

這兩個 URL 應該按相同處理,畢竟它們有徹底同樣的屬性。但在JavaScript中,它們是各自獨立、互不相同的,而且,絕對沒有辦法來重載相等運算符。

其它一些語言就支持這一特性。在Java, Python, Ruby中,每一個類均可以重載它的相等運算符;Scheme的許多實現中,每一個哈希表能夠使用不一樣的相等關係。C++則二者都支持。

可是,全部這些機制都須要編寫者自行實現一個哈希函數並暴露出系統默認的哈希函數。在JS中,由於不得不考慮其它語言沒必要擔憂的互用性和安全性,委員會選擇了不暴露——至少目前仍如此。

JS是不一樣的,第二部分:意料以外的可預測性

你多半以爲一臺計算機具備肯定性行爲是理所應當的,但當我告訴別人遍歷Map或Set的順序就是其中元素的插入順序時,他們老是很驚奇。沒錯,它就是肯定的。

咱們已經習慣了哈希表某些方面任性的行爲,咱們學會了接受它。不過,總有一些足夠好的理由讓咱們但願嘗試避免這種不肯定性。2012年我寫過:

  • 有證據代表,部分程序員一開始會以爲遍歷順序的不肯定性是使人驚奇又困惑的。1 2 3 4 5 6
  • ECMAScript中沒有明確規定遍歷屬性的順序,但爲了兼容互聯網現狀,幾乎全部主流實現都不得不將其定義爲插入順序。所以,有人擔憂,假如TC39不確立一個肯定的遍歷順序,「互聯網社區也會在自行發展中替咱們決定。」 7
  • 自定義哈希表的遍歷順序會暴露一些哈希對象的代碼,繼而引起關於哈希函數實現的一些惱人的安全問題。例如,暴露出的代碼毫不能獲知一個對象的地址。(向不受信任的ES代碼透露對象地址而對其自身隱藏,將是互聯網的一大安全漏洞。)

在2012年2月以上種種意見被提出時,我是支持不肯定遍歷序的。而後,我決定用實驗證實,保存插入序將過分下降哈希表的效率。我寫了一個C++的小型基準測試,結果卻令我驚奇地偏偏相反

這就是咱們最終爲JS設計了按插入序遍歷的哈希表的過程。

推薦使用弱集合的重要緣由

上篇文章咱們討論了一個JS動畫庫相關的例子。咱們試着要爲每一個DOM對象設置一個布爾值類型的標識屬性,就像這樣:

if (element.isMoving) {
  smoothAnimations(element);
}
element.isMoving = true;

不幸的是,這樣給一個DOM對象增長屬性不是個好主意。緣由咱們上次已經解釋過了。

上次的文章裏,咱們接着展現了用Symbol解決這個問題的方法。可是,能夠用集合來實現一樣的效果麼?也許看上去會像這樣:

if (movingSet.has(element)) {
  smoothAnimations(element);
}
movingSet.add(element);

這隻有一個壞處。Map和Set都爲內部的每一個鍵或值保持了強引用,也就是說,若是一個DOM元素被移除了,回收機制沒法取回它佔用的內存,除非movingSet中也刪除了它。在最理想的狀況下,庫在善後工做上對使用者都有複雜的要求,因此,這極可能引起內存泄露。

ES6給了咱們一個驚喜的解決方案:用WeakSet而非Set。和內存泄露說再見吧!

也 就是說,這個特定情景下的問題能夠用弱集合(weak collection)或Symbol兩種方法解決。哪一個更好呢?不幸的是,完整地討論利弊取捨會把這篇文章拖得有些長。簡而言之,若是能在整個網頁的生 命週期內使用同一個Symbol,那就沒什麼問題;若是不得不使用一堆臨時的Symbol,那就危險了,是時候考慮WeakMap來避免內存泄露了。

WeakMap和WeakSet

WeakMap和WeakSet被設計來完成與Map、Set幾乎同樣的行爲,除了如下一些限制:

  • WeakMap只支持new、has、get、set 和delete。
  • WeakSet只支持new、has、add和delete。
  • WeakSet的值和WeakMap的鍵必須是對象。

還要注意,這兩種弱集合都不可迭代,除非專門查詢或給出你感興趣的鍵,不然不能得到一個弱集合中的項。

這些當心設計的限制讓垃圾回收機制能回收仍在使用中的弱集合裏的無效對象。這效果相似於弱引用或弱鍵字典,但ES6的弱集合能夠在不暴露腳本中正在垃圾回收的前提下獲得垃圾回收的效益。

JS是不一樣的,第三部分:隱藏垃圾回收的不肯定性

弱集合其實是用 ephemeron 表實現的。

簡單說,一個WeakSet並不對其中對象保持強引用。當WeakSet中的一個對象被回收時,它會簡單地被從WeakSet中移除。WeakMap也相似地不爲它的鍵保持強引用。若是一個鍵仍被使用,相應的值也就仍被使用。

爲何要接受這些限制呢?爲何不直接在JS中引入弱引用呢?

再 次地,這是由於標準委員會很不肯意向腳本暴露未定義行爲。孱弱的跨瀏覽器兼容性是互聯網發展的痛苦之源。弱引用暴露了底層垃圾回收的實現細節——這正是與 平臺相關的一個未定義行爲。應用固然不該該依賴平臺相關的細節,但弱引用使咱們難於精確瞭解本身對測試使用的瀏覽器的依賴程度。這是件很不講道理的事情。

相比之下,ES6的弱集合只包含了一套有限的特性,但它們至關牢靠。一個鍵或值被回收從不會被觀測到,因此應用將不會依賴於其行爲,即便只是緣於意外。

這是針對互聯網的特殊考量引起了一個驚人的設計、進而使JS成爲一門更好語言的一個例子。

何時能夠用上這些集合呢?

總計四種集合類在Firefox、Chrome、Microsoft Edge、Safari中都已實現,要支持舊瀏覽器則須要 ES6 - Collections 之類來補全。

Firefox中的WeakMap 最初由 Andreas Gal 實現,他後來當了一段時間Mozilla的CTO。Tom Schuster實現了WeakSet,我實現了Map和Set。感謝Tooru Fujisawa貢獻的幾個相關補丁。

學習Babel和Broccoli,立刻就用ES6


自ES6正式發佈,人們已經開始討論ES7:將來版本會保留哪些特性,新標準可能提供什麼樣的新特性。做爲Web開發者,咱們想知道如何發揮這一切的巨大能量。在深刻淺出ES6系列以前的文章中,咱們不斷鼓勵你開始在編碼中加入ES6新特性,輔以一些有趣的工具,你徹底能夠從如今開始使用ES6:

若是你想在Web端使用這種新語法,你能夠經過Babel或Google的Traceur將你的ES6代碼轉譯爲Web友好的ES5代碼。

如今,咱們將向你分步展現如何作到的這一切。上面說起的工具被稱爲轉譯器,你能夠將它理解爲源代碼到源代碼的編譯器——一個在可比較的抽象層上操做不一樣編程語言相互轉換的編譯器。轉譯器容許咱們用ES6編寫代碼,同時保證這些代碼能在每個瀏覽器中執行。

轉譯技術拯救了咱們

轉譯器使用起來很是簡單,只需兩步便可描述它所作的事情:

1,用ES6的語法編寫代碼。

let q = 99;
let myVariable = `${q} bottles of beer on the wall, ${q} bottles of beer.`;

2,用上面那段代碼做爲轉譯器的輸入,通過處理後獲得如下這段輸出:

"use strict";
 
var q = 99;
var myVariable = "" + q + " bottles of beer on the wall, " + q + " bottles of beer."

這正是咱們熟知的老式JavaScript,這段代碼能夠在任意瀏覽器中運行。

轉譯器內部從輸入到輸出的邏輯高度複雜,徹底超出本篇文章的講解範圍。正如咱們無須知道全部的內部引擎結構就能夠駕駛一輛汽車,如今,咱們一樣能夠將轉譯器視爲一個可以處理咱們代碼的黑盒。

實際體驗Babel

你能夠經過幾種不一樣的方法在項目中使用Babel,有一個命令行工具,在這個工具中能夠使用以下形式的指令:

babel script.js --out-file script-compiled.js

Babel也提供支持在瀏覽器中使用的版本。你能夠將Babel做爲一個普通的庫引入,而後將你的ES6代碼放置在類型爲text/babel的script標籤中。

<script src="node_modules/babel-core/browser.js"></script>
<script type="text/babel">
// 你的ES6代碼
</script>
隨着代碼庫爆炸式增加,你開始將全部代碼劃分爲多個文件和文件夾,可是這些方法並不能隨之擴展。到那時,你將須要一個構建工具以及一種將Babel與構建管道整合在一塊兒的方法。

在接下來的章節中,咱們將要把Babel整合到構建工具Broccoli.js中,咱們將在兩個示例中編寫並執行第一行ES6代碼。若是你的代碼沒法正常運行,能夠在這裏(broccoli-babel-examples)查看完整的源代碼。在這個倉庫中你能夠找到三個示例項目:

  1. es6-fruits
  2. es6-website
  3. es6-modules

每個項目都構建於前一個示例的基礎之上,咱們會從最小的項目開始,逐步得出一個通常的解決方案,爲往後每個雄心壯志的項目打下良好的開端。這篇文章只包含前兩個示例,閱讀文章後,你徹底能夠自行閱讀第三個示例中的代碼並加以理解。

若是你在想——我坐等瀏覽器支持這些新特性就行了啦——那麼你必定會落後的!實現全部功能要花費很長時間,何況如今有成熟的轉譯器,並且 ECMAScript加快了發佈新版本的週期(每一年一版),咱們將會看到新標準比統一的瀏覽器平臺更新得更頻繁。因此趕快加入咱們,一塊兒發揮新特性的巨大威力吧!

咱們的首個Broccoli與Babel項目

Broccoli是一個用來快速構建項目的工具,你能夠用它對文件進行混淆與壓縮,還能夠經過衆多的Broccoli插件實現許多其它功能。它幫助咱們處理文件和目錄,每當項目變動時自動執行指令,很大程度上減輕了咱們的負擔。你不妨將它視爲:

相似Rails的asset管道,可是Broccoli運行在Node上且能夠對接任意後端。

配置項目

NODE

你可能已經猜到了,你須要安裝Node 0.11或更高版本

若是你使用unix系統,不要從包管理器(apt、yum等)中安裝,這樣能夠避免在安裝過程當中使用root權限,最好使用當前的用戶權限,經過上面的連接手動安裝。在文章《不要sudo npm》中能夠了解爲何不推薦使用root權限,文章中也給出了其它安裝方案

BROCCOLI

首先,咱們要配置好Broccoli項目:

mkdir es6-fruits
cd es6-fruits
npm init
# 建立一個名爲Brocfile.js的空文件
touch Brocfile.js

如今咱們安裝broccolibroccoli-cli

# 安裝broccoli庫
npm install --save-dev broccoli
# 命令行工具
npm install -g broccoli-cli

編寫一些ES6代碼

建立src文件夾,在裏面置入fruits.js文件。

mkdir src
vim src/fruits.js

用ES6語法在新文件中寫一小段腳本。

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}`);

上面的代碼示例使用了三個ES6特性:

  1. let進行局部做用域聲明(在稍後的文章中討論)
  2. for-of循環
  3. 模板字符串

保存文件,嘗試執行腳本。

node src/fruits.js

目前這段代碼不能正常運行,可是咱們將會讓它運行在Node與任何瀏覽器中。

let fruits = [
    ^^^^^^
SyntaxError: Unexpected identifier

轉譯時刻

如今,咱們用Broccoli加載代碼,而後用Babel處理它。編輯Brocfile.js文件並加入如下這段代碼:

// 引入babel插件
var babel = require('broccoli-babel-transpiler');
 
// 獲取源代碼,執行轉譯指令(僅需1步)
fruits = babel('src'); // src/*.js
 
module.exports = fruits;

注意咱們引入了包裹在Babel庫中的Broccoli插件broccoli-babel-transpiler,因此咱們必定要安裝它:

npm install --save-dev broccoli-babel-transpiler

如今咱們能夠構建項目並執行腳本了:

broccoli build dist # 編譯
node dist/fruits.js # 執行ES5

輸出結果看起來應當是這樣的:

ID: 100 Name: 草莓
ID: 101 Name: 柚子
ID: 102 Name: 李子
List total: 3

那很簡單!你能夠打開dist/fruits.js查看轉譯後代碼。Babel轉譯器的一個優秀特性是它可以生產可讀的代碼。

爲網站編寫ES6代碼

在第二個示例中,咱們將作進一步提高。首先,退出es6-fruits文件夾,而後使用上述配置項目一章中列出的步驟建立新目錄es6-website

在src文件夾中建立三個文件: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構建的' +
          '最小網站示例</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);

你可能已經注意到function* hexRange,是的,那是ES6的生成器。這個特性目前還沒有被全部瀏覽器支持。爲了可以使用這個特性,咱們須要一個polyfill,Babel中已經支持,咱們很快將投入使用。

下一步是合併全部JS文件而後在網站中使用。最難的部分是編寫Brocfile文件,這一次咱們要安裝4個插件:

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轉譯器
var babel = require('broccoli-babel-transpiler');
// 過濾樹(文件的子集)
var funnel = require('broccoli-funnel');
// 連結樹
var concat = require('broccoli-concat');
// 合併樹
var mergeTrees = require('broccoli-merge-trees');
 
// 轉譯源文件
var appJs = babel('src');
 
// 獲取Babel庫提供的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']
});
 
// 給轉譯後的文件樹添加Babel polyfill
appJs = mergeTrees([browserPolyfill, appJs]);
 
// 將全部JS文件連結爲一個單獨文件
appJs = concat(appJs, {
  // 咱們指定一個連結順序
  inputFiles: ['browser-polyfill.js', '**/*.js'],
  outputFile: '/js/my-app.js'
});
 
// 獲取入口文件
var index = funnel('src', {files: ['index.html']});
 
// 獲取全部的樹
// 並導出最終單一的樹
module.exports = mergeTrees([index, appJs]);

如今開始構建並執行咱們的代碼。

broccoli build dist

此次你在dist文件夾中應該看到如下結構:

$> tree dist/
dist/
├── index.html
└── js
    └── my-app.js

那是一個靜態網站,你能夠用任意服務器伺服來驗證那段代碼正常運行。舉個例子:

cd dist/
python -m SimpleHTTPServer
# 訪問http://localhost:8000/

你應該能夠看到:

Babel和Broccoli組合還有更多樂趣

上述第二個示例給出了一個經過Babel實現功能的思路,它可能足夠你用上一陣子了。若是你想要更多有關ES六、Babel和Broccoli的內容,能夠查看broccoli-babel-boilerplate,這個倉庫中的代碼能夠提供Broccoli+Babel項目的配置,並且高出至少兩個層次。這個樣板能夠文件處理模塊、模塊導入以及單元測試。

經過這些配置,你能夠在示例es6-modules中親自實踐。Brocfile魔力無窮,與咱們以前實現的很是相似。

正如你看到的,Babel和Broccoli對於在Web網站中應用ES6新特性很是實用。

代理 Proxies


請看這樣一段代碼:

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);
  }
});

代碼乍一看有些複雜,使用了一些陌生的特性,稍後我會詳細講解每一部分。如今,一塊兒來看一下咱們建立的對象:

> obj.count = 1;
    setting count!
> ++obj.count;
    getting count!
    setting count!
    2

顯示結果可能與咱們的理解不太同樣,爲何會輸出「setting count」和「getting count」?其實,咱們攔截了這個對象的屬性訪問方法,而後將「.」運算符重載了。

它是如何作到的?

計算領域最好的的技巧是虛擬化,這種技術通常用來實現驚人的功能。它的工做機制以下:

  1. 隨便選一張照片。

    圖片來源:Martin Nikolaj Bech

  2. 在圖片中圍繞某物勾勒出一個輪廓。

  3. 如今替換掉輪廓中的內容,或者替換掉輪廓外的內容,可是始終要遵循向後兼容的規則,替換先後的圖片要儘量類似,不能讓輪廓兩側的圖像過於突兀。

    圖片來源:Beverley Goodwin

你可能在《楚門的世界》和《黑客帝國》這類經典的計算機科學電影中見到過相似的hack方法,將世界劃分爲兩個部分,主人公生活在內部世界,外部世界被精心編造的常態幻覺所替換。

爲了知足向後兼容的規則,你須要巧妙地設計填補進去的圖片,可是真正的技巧是正確地勾勒輪廓。

我所謂的輪廓是指一個API邊界或接口,接口能夠詳細說明兩段代碼的交互方式以及交互雙方對另外一半的需求。因此若是一旦在系統中設計好了接口,輪廓天然就清晰了,這樣就能夠任意替換接口兩側的內容而不影響兩者的交互過程。

若是沒有現成的接口,就須要施展你的創意才華來創造新接口,有史以來最酷的軟件hack老是會勾勒一些以前從未有過的API邊界,而後經過大量的工程化實踐將接口引入到現有的體系中去。

虛擬內存硬件虛擬化DockerValgrindrr等不一樣抽象程度的項目都會基於現有的系統推進開發一些使人意想不到的新接口。在某些狀況下,須要花費數年的時間、新的操做系統特性甚至是新的硬件來使新的邊界良好運轉。

最棒的虛擬化hack會帶來對須要虛擬的東西的新的理解。想要編寫一個API,你須要充分理解你所面向的對象,一旦你理解透徹,就能實現出使人驚異的成果。

而ES6則爲JavaScript中最基本的概念「對象(object)」引入了虛擬化支持。

因此,對象究竟是什麼?

噢,我是說真的,請花費一點時間仔細想一想這個問題的答案。當你清楚本身知道對象是什麼的的時候再向下滾動。

這個問題於我而言太難了!我從未聽到過一個很是滿意的定義。

這會讓你感到驚訝麼?定義基礎概念向來很困難——抽空看看歐幾里得在《幾何本來》中的前幾個定義你就知道了。ECMAScript語言規範很棒,但是卻將對象定義爲「type對象的成員」,這種定義真的對咱們沒什麼幫助。

後來,規範中又添加了一個定義:「對象是屬性的集合」。這句話沒錯,目前來講能夠這樣定義,咱們稍後繼續討論。

我以前說過,想要編寫一個API,你須要充分理解你所面向的對象。因此在某種程度上,我也算對本文作出一個承諾,咱們會一塊兒深刻理解對象的細節,而後一塊兒實現酷炫的功能。

那麼咱們就跟隨ECMAScript標準委員會的腳步,爲JavaScript對象定義一個API,一個接口。問題是咱們須要什麼方法?對象又能夠作什麼呢?

這個問題的答案必定程度上取決於對象的類型:DOM元素對象能夠作一部分事情,音頻節點對象又能夠作另一部分事情,可是全部對象都會共享一些基礎功能:

  • 對象都有屬性。你能夠get、set或刪除它們或作更多操做。
  • 對象都有原型。這也是JS中繼承特性的實現方式。
  • 有一些對象是能夠被調用的函數或構造函數。

幾乎全部處理對象的JS程序都是使用屬性、原型和函數來完成的。甚至元素或聲音節點對象的特殊行爲也是經過調用繼承自函數屬性的方法來進行訪問。

因此ECMAScript標準委員會定義了一個由14種內部方法組成的集合,亦即一個適用於全部對象的通用接口,屬性、原型和函數這三種基礎功能天然成爲它們關注的核心。

咱們能夠在ES6標準列表5和6中找到所有的14種方法,我只會在這裏講解其中一部分。雙方括號[[ ]]表明內部方法,在通常的JS代碼中不可見,你能夠調用、刪除或覆寫普通方法,可是沒法操做內部方法。

  • obj.[[Get]](key, receiver) – 獲取屬性值。

    當JS代碼執行如下方法時被調用:obj.propobj[key]

    obj是當前被搜索的對象,receiver是咱們首先開始搜索這個屬性的對象。有時咱們必需要搜索幾個對象,obj多是一個在receiver原型鏈上的對象。

  • obj.[[Set]](key, value, receiver) – 爲對象的屬性賦值。

    當JS代碼執行如下方法時被調用:obj.prop = valueobj[key] = value

    執行相似obj.prop += 2這樣的賦值語句時,首先調用[[Get]]方法,而後調用[[Set]]方法。對於++和--操做符來講亦是如此。

  • obj.[HasProperty] – 檢測對象中是否存在某屬性。

    當JS代碼執行如下方法時被調用:key in obj

  • obj.[Enumerate] – 列舉對象的可枚舉屬性。

    當JS代碼執行如下方法時被調用:for (key in obj)

    這個內部方法會返回一個可迭代對象,for-in循環可經過這個方法獲得對象屬性的名稱。

  • obj.[GetPrototypeOf] – 返回對象的原型。

    當JS代碼執行如下方法時被調用:obj.[__proto__]Object.getPrototypeOf(obj)

  • functionObj.[[Call]](thisValue, arguments) – 調用一個函數。

    當JS代碼執行如下方法時被調用:functionObj()x.method()

    可選的。不是每個對象都是函數。

  • constructorObj.[[Construct]](arguments, newTarget) – 調用一個構造函數。

    當JS代碼執行如下方法時被調用:舉個例子,new Date(2890, 6, 2)

    可選的。不是每個對象都是構造函數。

    參數newTarget在子類中起必定做用,咱們將在將來的文章中詳細講解。

可能你也能夠猜到其它七個內部方法。

在整個ES6標準中,只要有可能,任何語法或對象相關的內建函數都是基於這14種內部方法構建的。ES6在對象的中樞系統周圍劃分了一個清晰的界限,你能夠藉助代理特性用任意JS代碼替換標準中樞系統的內部方法。

既然咱們立刻要開始討論覆寫內部方法的相關問題,請記住,咱們要討論的是諸如obj.prop的核心語法、諸如Object.keys()的內建函數等的行爲。

代理 Proxy

ES6規範定義了一個全新的全局構造函數:代理Proxy)。它能夠接受兩個參數:目標對象(target)句柄對象(handler)。請看一個簡單的示例:

var target = {}, handler = {};
var proxy = new Proxy(target, handler);

咱們先來探討代理目標對象之間的關係,而後再研究句柄對象的功用。

代理的行爲很簡單:將代理的全部內部方法轉發至目標。簡單來講,若是調用proxy.[[Enumerate]](),就會返回target.[[Enumerate]]()

如今,讓咱們嘗試執行一條可以觸發調用proxy.[[Set]]()方法的語句。

proxy.color = "pink";

好的,剛剛都發生了什麼?proxy.[[Set]]()應該調用target.[[Set]]()方法,而後在目標上建立一個新的屬性。實際的結果如何?

> target.color
    "pink"

是的,它作到了!對於全部其它內部方法而言一樣能夠作到。新建立的代理會盡量與目標的行爲一致。

固然,它們也不徹底相同,你會發現proxy !== target。有時也有目標可以經過類型檢測而代理沒法經過的狀況發生,舉個例子,若是代理的目標是一個DOM元素,相應的代理就不是,此時相似document.body.appendChild(proxy)的操做會觸發類型錯誤(TypeError)。

代理句柄

如今咱們繼續來討論一個讓代理充滿魔力的功能:句柄對象。

句柄對象的方法能夠覆寫任意代理的內部方法。

舉個例子,你能夠定義一個handler.set()方法來攔截全部給對象屬性賦值的行爲:

var target = {};
var handler = {
  set: function (target, key, value, receiver) {
    throw new Error("請不要爲這個對象設置屬性。");
  }
};
var proxy = new Proxy(target, handler);
> proxy.name = "angelina";
    Error: 請不要爲這個對象設置屬性。

句柄方法的完整列表能夠在MDN有關代理的頁面上找到,一共有14種方法,與ES6中定義的14中內部方法一致。

全部句柄方法都是可選的,沒被句柄攔截的內部方法會直接指向目標,與咱們以前看到的別無二致。

小試牛刀(一):「不可能實現的」自動填充對象

到目前爲止,咱們對於代理的瞭解程度足夠嘗試去作一些奇怪的事情,實現一些不借助代理根本沒法實現的功能。

咱們的第一個實踐,建立一個Tree()函數來實現如下特性:

> 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" }}}

請注意,當咱們須要時,全部中間對象branch1branch2branch3均可以自動建立。這當然很方便,可是如何實現呢?

在這以前,沒有能夠實現這種特性的方法,可是經過代理,咱們只用寥寥幾行就能夠輕鬆實現,而後只須要接入tree.[[Get]]()就能夠。若是你喜歡挑戰,在繼續閱讀前能夠嘗試本身實現。

這裏是個人解決方案:

function Tree() {
  return new Proxy({}, handler);
}
var handler = {
  get: function (target, key, receiver) {
    if (!(key in target)) {
      target[key] = Tree();  // 自動建立一個子樹
    }
    return Reflect.get(target, key, receiver);
  }
};

注意最後的Reflect.get()調用,在代理句柄方法中有一個極其常見的需求:只執行委託給目標的默認行爲。因此ES6定義了一個新的反射(Reflect)對象,在其上有14種方法,你能夠用它來實現這一需求。

小試牛刀(二):只讀視圖

我想我可能傳達給大家一個錯誤的印象,也就是代理易於使用。接下來的這個示例可能會讓你稍感困頓。

這一次咱們的賦值語句更復雜:咱們須要實現一個函數,readOnlyView(object),它能夠接受任何對象做爲參數,並返回一個與此對象行爲一致的代理,該代理不可被變動,就像這樣:

> 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

咱們如何實現這樣的功能?

即便咱們不會阻斷內部方法的行爲,但仍然要對其進行干預,因此第一步是攔截可能修改目標對象的五種內部方法。

function NOPE() {
  throw new Error("can't modify read-only view");
}
var handler = {
  // 覆寫全部五種可變方法。
  set: NOPE,
  defineProperty: NOPE,
  deleteProperty: NOPE,
  preventExtensions: NOPE,
  setPrototypeOf: NOPE
};
function readOnlyView(target) {
  return new Proxy(target, handler);
}

這段代碼能夠正常運行,它藉助只讀視圖阻止了賦值、屬性定義等過程。

這種方案中是否有漏洞?

最大的問題是相似[[Get]]的一些方法可能仍然返回可變對象,因此即便一些對象x是隻讀視圖,x.prop多是可變的!這是一個巨大的漏洞。

咱們須要添加一個handler.get()方法來堵上漏洞:

var handler = {
  ...
  // 在只讀視圖中包裹其它結果。
  get: function (target, key, receiver) {
    // 從執行默認行爲開始。
    var result = Reflect.get(target, key, receiver);
    // 確保返回一個不可變對象!
    if (Object(result) === result) {
      // result是一個對象。
      return readOnlyView(result);
    }
    // result是一個原始原始類型,因此已經具有不可變的性質。
    return result;
  },
  ...
};

這仍然不夠,getPrototypeOfgetOwnPropertyDescriptor這兩個方法也須要進行一樣的處理。

然而還有更多問題,當經過這種代理調用getter或方法時,傳遞給getter或方法的this的值一般是代理自身。可是正如咱們以前所見,有時代理沒法經過訪問器和方法執行的類型檢查。在這裏用目標對象代替代理更好一些。聰明的小夥伴,你知道如何解決這個問題麼?

因而可知,建立代理很是簡單,可是建立一個具備直觀行爲的代理至關困難。

隻言片語

  • 代理到底好在哪裏?

    代理能夠幫助你觀察或記錄對象訪問,當調試代碼時助你一臂之力,測試框架也能夠用代理來建立模擬對象mock object)。

    代理能夠幫助你強化普通對象的能力,例如:惰性屬性填充。

    我不太想提到這一點,可是若是要想了解代理在代碼中的運行方式,將代理的句柄對象包裹在另外一個代理中是一個很是不錯的辦法,每當句柄方法被訪問時就能夠將你想要的信息輸出到控制檯中。

    正如上文中只讀視圖的示例readOnlyView,咱們能夠用代理來限制對象的訪問。固然在應用代碼中不多遇到這種用例,可是Firefox在內部使用代理來實現不一樣域名之間的安全邊界,是咱們的安全模型的關鍵組成部分。

  • 與WeakMap深度結合。在咱們的readOnlyView示例中,每當對象被訪問的時候建立一個新的代理。這種作法能夠幫助咱們節省在WeakMap中建立代理時的緩存內存,因此不管傳遞多少次對象給readOnlyView,只會建立一個代理。

    這也是一個動人的WeakMap用例。

  • 代理可解除。ES6規範中還定義了另一個函數:Proxy.revocable(target, handler)。這個函數能夠像new Proxy(target, handler)同樣建立代理,可是建立好的代理後續可被解除。(Proxy.revocable方法返回一個對象,該對象有一個.proxy屬性和一個.revoke方法。)一旦代理被解除,它即刻中止運行並拋出全部內部方法。

  • 對象不變性。在某些狀況下,ES6須要代理的句柄方法來報告與目標對象狀態一致的結果,以此來保證全部對象甚至是代理的不變性。舉個例子,除非目標不可擴展(inextensible),不然代理不能被聲明爲不可擴展的。

不變性的規則很是複雜,在此不展開詳述,可是若是你看到相似「proxy can't report a non-existent property as non-configurable」這樣的錯誤信息,就能夠考慮從不變性的角度解決問題,最可能的補救方法是改變代理報告自己,或者在運行時改變目標對象來反射代理的報告指向。

如今,你認爲對象是什麼?

我記得咱們以前的看法是:「對象是屬性的集合。」

我不喜歡這個定義,即便給定義疊加原型和可調用能力也不會讓我改變見解。我認爲「集合(collection)」這個詞太危險了,不適合用做對象的定義。對象的句柄方法能夠作任何事情,它們也能夠返回隨機結果。

ECMAScript標準委員會針對這個問題開展了許多研究,搞清楚了對象能作的事情,將那些方法進行標準化,並將虛擬化技術做爲每一個人都能使用的一等特性添加到語言的新標準中,爲前端開發領域拓展了無限可能。

完善後的對象幾乎能夠表示任何事物。

對象是什麼?可能如今最貼切的答案須要用12個內部方法進行定義:對象是在JS程序中擁有[[Get]]、[[Set]]等操做的實體。

我不太肯定咱們是否比以前更瞭解對象,可是咱們絕對作了許多驚豔的事情,是的,咱們實現了舊版JS根本作不到的功能。

我如今能夠使用代理麼?

不!在Web平臺上不管如何都不行。目前只有Firefox和微軟的Edge支持代理,並且尚未支持這一特性polyfill。

若是你想在Node.js或io.js環境中使用代理,首先你須要添加名爲harmony-reflect的polyfill,而後在執行時啓用一個非默認的選項(--harmony_proxies),這樣就能夠暫時使用V8中實現的老版本代理規範。

放輕鬆,讓咱們一塊兒來作試驗吧!爲每個對象建立成千上萬個類似的副本鏡像卻不能調試?如今就解放本身!不過目前來看,請不要將欠考慮的有關代理的代碼泄露到產品中,這很是危險。

代理特性在2010年由Andreas Gal首先實現,由Blake Kaplan進行代碼審查。標準委員會後來徹底從新設計了這個特性。Eddy Bruel在2012年實現了新標準。

我實現了反射(Reflect)特性,由Jeff Walden進行代碼審查。Firefox Nightly已經支持除Reflect.enumerate()外的全部特性。

相關文章
相關標籤/搜索