# 第二章:語法
如果你曾經或多或少地寫過JS,那么你很可能對它的語法感到十分熟悉。當然有一些奇怪之處,但是總體來講這是一種與其他語言有很多相似之處的,相當合理而且直接的語法。
然而,ES6增加了好幾種需要費些功夫才能習慣的新語法形式。在這一章中,我們將遍歷它們來看看葫蘆里到底賣的什么藥。
提示:?在寫作本書時,這本書中所討論的特性中的一些已經被各種瀏覽器(Firefox,Chrome,等等)實現了,但是有一些僅僅被實現了一部分,而另一些根本就沒實現。如果直接嘗試這些例子,你的體驗可能會夾雜著三種情況。如果是這樣,就使用轉譯器嘗試吧,這些特性中的大多數都被那些工具涵蓋了。ES6Fiddle(http://www.es6fiddle.net/ )是一個了不起的嘗試ES6的游樂場,簡單易用,它是一個Babel轉譯器的在線REPL(http://babeljs.io/repl/ )。
## 塊兒作用域聲明
你可能知道在JavaScript中變量作用域的基本單位總是`function`。如果你需要創建一個作用域的塊兒,除了普通的函數聲明以外最流行的方法就是使用立即被調用的函數表達式(IIFE)。例如:
```source-js
var a = 2;
(function IIFE(){
var a = 3;
console.log( a ); // 3
})();
console.log( a ); // 2
```
### `let`聲明
但是,現在我們可以創建綁定到任意的塊兒上的聲明了,它(勿庸置疑地)稱為?*塊兒作用域*。這意味著一對`{ .. }`就是我們用來創建一個作用域所需要的全部。`var`總是聲明附著在外圍函數(或者全局,如果在頂層的話)上的變量,取而代之的是,使用`let`:
```source-js
var a = 2;
{
let a = 3;
console.log( a ); // 3
}
console.log( a ); // 2
```
迄今為止,在JS中使用獨立的`{ .. }`塊兒不是很常見,也不是慣用模式,但它總是合法的。而且那些來自擁有?*塊兒作用域*?的語言的開發者將很容易認出這種模式。
我相信使用一個專門的`{ .. }`塊兒是創建塊兒作用域變量的最佳方法。但是,你應該總是將`let`聲明放在塊兒的最頂端。如果你有多于一個的聲明,我推薦只使用一個`let`。
從文體上說,我甚至喜歡將`let`放在與開放的`{`的同一行中,以便更清楚地表示這個塊兒的目的僅僅是為了這些變量聲明作用域。
```source-js
{ let a = 2, b, c;
// ..
}
```
它現在看起來很奇怪,而且不大可能與其他大多數ES6文獻中推薦的文法吻合。但我的瘋狂是有原因的。
這是另一種實驗性的(不是標準化的)`let`聲明形式,稱為`let`塊兒,看起來就像這樣:
```source-js
let (a = 2, b, c) {
// ..
}
```
我稱這種形式為?*明確的*?塊兒作用域,而與`var`相似的`let`聲明形式更像是?*隱含的*,因為它在某種意義上劫持了它所處的`{ .. }`。一般來說開發者們認為?*明確的*?機制要比?*隱含的*?機制更好一些,我主張這種情況就是這樣的情況之一。
如果你比較前面兩個形式的代碼段,它們非常相似,而且我個人認為兩種形式都有資格在文體上稱為?*明確的*?塊兒作用域。不幸的是,兩者中最?*明確的*?`let (..) { .. }`形式沒有被ES6所采用。它可能會在后ES6時代被重新提起,但我想目前為止前者是我們的最佳選擇。
為了增強對`let ..`聲明的?*隱含*?性質的理解,考慮一下這些用法:
```source-js
let a = 2;
if (a > 1) {
let b = a * 3;
console.log( b ); // 6
for (let i = a; i <= b; i++) {
let j = i + 10;
console.log( j );
}
// 12 13 14 15 16
let c = a + b;
console.log( c ); // 8
}
```
不要回頭去看這個代碼段,小測驗:哪些變量僅存在于`if`語句內部?哪些變量僅存在于`for`循環內部?
答案:`if`語句包含塊兒作用域變量`b`和`c`,而`for`循環包含塊兒作用域變量`i`和`j`。
你有任何遲疑嗎?`i`沒有被加入外圍的`if`語句的作用域讓你驚訝嗎?思維上的停頓和疑問 —— 我稱之為“思維稅” —— 不僅源自于`let`機制對我們來說是新東西,還因為它是?*隱含的*。
還有一個災難是`let c = ..`聲明出現在作用域中太過靠下的地方。傳統的被`var`聲明的變量,無論它們出現在何處,都會被附著在整個外圍的函數作用域中;與此不同的是,`let`聲明附著在塊兒作用域,而且在它們出現在塊兒中之前是不會被初始化的。
在一個`let ..`聲明/初始化之前訪問一個用`let`聲明的變量會導致一個錯誤,而對于`var`聲明來說這個順序無關緊要(除了文體上的區別)。
考慮如下代碼:
```source-js
{
console.log( a ); // undefined
console.log( b ); // ReferenceError!
var a;
let b;
}
```
警告:?這個由于過早訪問被`let`聲明的引用而引起的`ReferenceError`在技術上稱為一個?*臨時死區(Temporal Dead Zone —— TDZ)*?錯誤 —— 你在訪問一個已經被聲明但還沒被初始化的變量。這將不是我們唯一能夠見到TDZ錯誤的地方 —— 在ES6中它們會在幾種地方意外地發生。另外,注意“初始化”并不要求在你的代碼中明確地賦一個值,比如`let b;`是完全合法的。一個在聲明時沒有被賦值的變量被認為已經被賦予了`undefined`值,所以`let b;`和`let b = undefined;`是一樣的。無論是否明確賦值,在`let b`語句運行之前你都不能訪問`b`。
最后一個坑:對于TDZ變量和未聲明的(或聲明的!)變量,`typeof`的行為是不同的。例如:
```source-js
{
// `a` 沒有被聲明
if (typeof a === "undefined") {
console.log( "cool" );
}
// `b` 被聲明了,但位于它的TDZ中
if (typeof b === "undefined") { // ReferenceError!
// ..
}
// ..
let b;
}
```
`a`沒有被聲明,所以`typeof`是檢查它是否存在的唯一安全的方法。但是`typeof b`拋出了TDZ錯誤,因為在代碼下面很遠的地方偶然出現了一個`let b`聲明。噢。
現在你應當清楚為什么我堅持認為所有的`let`聲明都應該位于它們作用域的頂部了。這完全避免了偶然過早訪問的錯誤。當你觀察一個塊兒,或任何塊兒的開始部分時,它還更?*明確*?地指出這個塊兒中含有什么變量。
你的塊兒(`if`語句,`while`循環,等等)不一定要與作用域行為共享它們原有的行為。
這種明確性要由你負責,由你用毅力來維護,它將為你省去許多重構時的頭疼和后續的麻煩。
注意:?更多關于`let`和塊兒作用域的信息,參見本系列的?*作用域與閉包*?的第三章。
####`let`?+?`for`
我偏好?*明確*?形式的`let`聲明塊兒,但對此的唯一例外是出現在`for`循環頭部的`let`。這里的原因看起來很微妙,但我相信它是更重要的ES6特性中的一個。
考慮如下代碼:
```source-js
var funcs = [];
for (let i = 0; i < 5; i++) {
funcs.push( function(){
console.log( i );
} );
}
funcs[3](); // 3
```
在`for`頭部中的`let i`不僅是為`for`循環本身聲明了一個`i`,而且它為循環的每一次迭代都重新聲明了一個新的`i`。這意味著在循環迭代內部創建的閉包都分別引用著那些在每次迭代中創建的變量,正如你期望的那樣。
如果你嘗試在這段相同代碼的`for`循環頭部使用`var i`,那么你會得到`5`而不是`3`,因為在被引用的外部作用域中只有一個`i`,而不是為每次迭代的函數都有一個`i`被引用。
你也可以稍稍繁冗地實現相同的東西:
```source-js
var funcs = [];
for (var i = 0; i < 5; i++) {
let j = i;
funcs.push( function(){
console.log( j );
} );
}
funcs[3](); // 3
```
在這里,我們強制地為每次迭代都創建一個新的`j`,然后閉包以相同的方式工作。我喜歡前一種形式;那種額外的特殊能力正是我支持`for(let .. ) ..`形式的原因。可能有人會爭論說它有點兒?*隱晦*,但是對我的口味來說,它足夠?*明確*?了,也足夠有用。
`let`在`for..in`和`for..of`(參見“`for..of`循環”)循環中也以形同的方式工作。
### `const`聲明
還有另一種需要考慮的塊兒作用域聲明:`const`,它創建?*常量*。
到底什么是一個常量?它是一個在初始值被設定后就成為只讀的變量。考慮如下代碼:
```source-js
{
const a = 2;
console.log( a ); // 2
a = 3; // TypeError!
}
```
變量持有的值一旦在聲明時被設定就不允許你改變了。一個`const`聲明必須擁有一個明確的初始化。如果想要一個持有`undefined`值的?*常量*,你必須聲明`const a = undefined`來得到它。
常量不是一個作用于值本身的制約,而是作用于變量對這個值的賦值。換句話說,值不會因為`const`而凍結或不可變,只是它的賦值被凍結了。如果這個值是一個復雜值,比如對象或數組,那么這個值的內容仍然是可以被修改的:
```source-js
{
const a = [1,2,3];
a.push( 4 );
console.log( a ); // [1,2,3,4]
a = 42; // TypeError!
}
```
變量`a`實際上沒有持有一個恒定的數組;而是持有一個指向數組的恒定的引用。數組本身可以自由變化。
警告:?將一個對象或數組作為常量賦值意味著這個值在常量的詞法作用域消失以前是不能夠被垃圾回收的,因為指向這個值的引用是永遠不能解除的。這可能是你期望的,但如果不是你就要小心!
實質上,`const`聲明強制實行了我們許多年來在代碼中用文體來表明的東西:我們聲明一個名稱全由大寫字母組成的變量并賦予它某些字面值,我們小心照看它以使它永不改變。`var`賦值沒有強制性,但是現在`const`賦值上有了,它可以幫你發現不經意的改變。
`const`*可以*?被用于`for`,`for..in`,和`for..of`循環(參見“`for..of`循環”)的變量聲明。然而,如果有任何重新賦值的企圖,一個錯誤就會被拋出,例如在`for`循環中常見的`i++`子句。
#### `const`用還是不用
有些流傳的猜測認為在特定的場景下,與`let`或`var`相比一個`const`可能會被JS引擎進行更多的優化。理論上,引擎可以更容易地知道變量的值/類型將永遠不會改變,所以它可以免除一些可能的追蹤工作。
無論`const`在這方面是否真的有幫助,還是這僅僅是我們的幻想和直覺,你要做的更重要的決定是你是否打算使用常量的行為。記住:源代碼扮演的一個最重要的角色是為了明確地交流你的意圖是什么,不僅是與你自己,而且還是與未來的你和其他的代碼協作者。
一些開發者喜歡在一開始將每個變量都聲明為一個`const`,然后當它的值在代碼中有必要發生變化的時候將聲明放松至一個`let`。這是一個有趣的角度,但是不清楚這是否真正能夠改善代碼的可讀性或可推理性。
就像許多人認為的那樣,它不是一種真正的?*保護*,因為任何后來的想要改變一個`const`值的開發者都可以盲目地將聲明從`const`改為`let`。它至多是防止意外的改變。但是同樣地,除了我們的直覺和感覺以外,似乎沒有客觀和明確的標準可以衡量什么構成了“意外”或預防措施。這與類型強制上的思維模式類似。
我的建議:為了避免潛在的令人糊涂的代碼,僅將`const`用于那些你有意地并且明顯地標識為不會改變的變量。換言之,不要為了代碼行為而?*依靠*?`const`,而是在為了意圖可以被清楚地表明時,將它作為一個表明意圖的工具。
### 塊兒作用域的函數
從ES6開始,發生在塊兒內部的函數聲明現在被明確規定屬于那個塊兒的作用域。在ES6之前,語言規范沒有要求這一點,但是許多實現不管怎樣都是這么做的。所以現在語言規范和現實吻合了。
考慮如下代碼:
```source-js
{
foo(); // 好用!
function foo() {
// ..
}
}
foo(); // ReferenceError
```
函數`foo()`是在`{ .. }`塊兒內部被聲明的,由于ES6的原因它是屬于那里的塊兒作用域的。所以在那個塊兒的外部是不可用的。但是還要注意它在塊兒里面被“提升”了,這與早先提到的遭受TDZ錯誤陷阱的`let`聲明是相反的。
如果你以前曾經寫過這樣的代碼,并依賴于老舊的非塊兒作用域行為的話,那么函數聲明的塊兒作用域可能是一個問題:
```source-js
if (something) {
function foo() {
console.log( "1" );
}
}
else {
function foo() {
console.log( "2" );
}
}
foo(); // ??
```
在前ES6環境下,無論`something`的值是什么`foo()`都將會打印`"2"`,因為兩個函數聲明被提升到了塊兒的頂端,而且總是第二個有效。
在ES6中,最后一行將拋出一個`ReferenceError`。
## 擴散/剩余
ES6引入了一個新的`...`操作符,根據你在何處以及如何使用它,它一般被稱作?*擴散(spread)*?或?*剩余(rest)*?操作符。讓我們看一看:
```source-js
function foo(x,y,z) {
console.log( x, y, z );
}
foo( ...[1,2,3] ); // 1 2 3
```
當`...`在一個數組(實際上,是我們將在第三章中講解的任何的?*可迭代*?對象)前面被使用時,它就將數組“擴散”為它的個別的值。
通常你將會在前面所展示的那樣的代碼段中看到這種用法,它將一個數組擴散為函數調用的一組參數。在這種用法中,`...`扮演了`apply(..)`方法的簡約語法替代品,在前ES6中我們經常這樣使用`apply(..)`:
```source-js
foo.apply( null, [1,2,3] ); // 1 2 3
```
但`...`也可以在其他上下文環境中被用于擴散/展開一個值,比如在另一個數組聲明內部:
```source-js
var a = [2,3,4];
var b = [ 1, ...a, 5 ];
console.log( b ); // [1,2,3,4,5]
```
在這種用法中,`...`取代了`concat(..)`,它在這里的行為就像`[1].concat( a, [5] )`。
另一種`...`的用法常見于一種實質上相反的操作;與將值散開不同,`...`將一組值?*收集*?到一個數組中。
```source-js
function foo(x, y, ...z) {
console.log( x, y, z );
}
foo( 1, 2, 3, 4, 5 ); // 1 2 [3,4,5]
```
這個代碼段中的`...z`實質上是在說:“將?*剩余的*?參數值(如果有的話)收集到一個稱為`z`的數組中。” 因為`x`被賦值為`1`,而`y`被賦值為`2`,所以剩余的參數值`3`,`4`,和`5`被收集進了`z`。
當然,如果你沒有任何命名參數,`...`會收集所有的參數值:
```source-js
function foo(...args) {
console.log( args );
}
foo( 1, 2, 3, 4, 5); // [1,2,3,4,5]
```
注意:?在`foo(..)`函數聲明中的`...args`經常因為你向其中收集參數的剩余部分而被稱為“剩余參數”。我喜歡使用“收集”這個詞,因為它描述了它做什么而不是它包含什么。
這種用法最棒的地方是,它為被廢棄了很久的`arguments`數組 —— 實際上它不是一個真正的數組,而是一個類數組對象 —— 提供了一種非常穩健的替代方案。因為`args`(無論你叫它什么 —— 許多人喜歡叫它`r`或者`rest`)是一個真正的數組,我們可以擺脫許多愚蠢的前ES6技巧,我們曾經通過這些技巧盡全力去使`arguments`變成我們可以視之為數組的東西。
考慮如下代碼:
```source-js
// 使用新的ES6方式
function foo(...args) {
// `args`已經是一個真正的數組了
// 丟棄`args`中的第一個元素
args.shift();
// 將`args`的所有內容作為參數值傳給`console.log(..)`
console.log( ...args );
}
// 使用老舊的前ES6方式
function bar() {
// 將`arguments`轉換為一個真正的數組
var args = Array.prototype.slice.call( arguments );
// 在末尾添加一些元素
args.push( 4, 5 );
// 過濾掉所有奇數
args = args.filter( function(v){
return v % 2 == 0;
} );
// 將`args`的所有內容作為參數值傳給`foo(..)`
foo.apply( null, args );
}
bar( 0, 1, 2, 3 ); // 2 4
```
在函數`foo(..)`聲明中的`...args`收集參數值,而在`console.log(..)`調用中的`...args`將它們擴散開。這個例子很好地展示了`...`操作符平行但相反的用途。
除了在函數聲明中`...`的用法以外,還有另一種`...`被用于收集值的情況,我們將在本章稍后的“太多,太少,正合適”一節中檢視它。
## 默認參數值
也許在JavaScript中最常見的慣用法之一就是為函數參數設置默認值。我們多年來一直使用的方法應當看起來很熟悉:
```source-js
function foo(x,y) {
x = x || 11;
y = y || 31;
console.log( x + y );
}
foo(); // 42
foo( 5, 6 ); // 11
foo( 5 ); // 36
foo( null, 6 ); // 17
```
當然,如果你曾經用過這種模式,你就會知道它既有用又有點兒危險,例如如果你需要能夠為其中一個參數傳入一個可能被認為是falsy的值。考慮下面的代碼:
```source-js
foo( 0, 42 ); // 53 <-- 噢,不是42
```
為什么?因為`0`是falsy,因此`x || 11`的結果為`11`,而不是直接被傳入的`0`。
為了填這個坑,一些人會像這樣更加啰嗦地編寫檢查:
```source-js
function foo(x,y) {
x = (x !== undefined) ? x : 11;
y = (y !== undefined) ? y : 31;
console.log( x + y );
}
foo( 0, 42 ); // 42
foo( undefined, 6 ); // 17
```
當然,這意味著除了`undefined`以外的任何值都可以直接傳入。然而,`undefined`將被假定是這樣一種信號,“我沒有傳入這個值。” 除非你實際需要能夠傳入`undefined`,它就工作的很好。
在那樣的情況下,你可以通過測試參數值是否沒有出現在`arguments`數組中,來看它是否實際上被省略了,也許是像這樣:
```source-js
function foo(x,y) {
x = (0 in arguments) ? x : 11;
y = (1 in arguments) ? y : 31;
console.log( x + y );
}
foo( 5 ); // 36
foo( 5, undefined ); // NaN
```
但是在沒有能力傳入意味著“我省略了這個參數值”的任何種類的值(連`undefined`也不行)的情況下,你如何才能省略第一個參數值`x`呢?
`foo(,5)`很誘人,但它不是合法的語法。`foo.apply(null,[,5])`看起來應該可以實現這個技巧,但是`apply(..)`的奇怪之處意味著這組參數值將被視為`[undefined,5]`,顯然它沒有被省略。
如果你深入調查下去,你將發現你只能通過簡單地傳入比“期望的”參數值個數少的參數值來省略末尾的參數值,但是你不能省略在參數值列表中間或者開頭的參數值。這就是不可能。
這里有一個施用于JavaScript設計的重要原則需要記住:`undefined`意味著?*缺失*。也就是,在`undefined`和?*缺失*?之間沒有區別,至少是就函數參數值而言。
注意:?容易令人糊涂的是,JS中有其他的地方不適用這種特殊的設計原則,比如帶有空值槽的數組。更多信息參見本系列的?*類型與文法*。
帶著所有這些認識,現在我們可以檢視在ES6中新增的一種有用的好語法,來簡化對丟失的參數值進行默認值的賦值。
```source-js
function foo(x = 11, y = 31) {
console.log( x + y );
}
foo(); // 42
foo( 5, 6 ); // 11
foo( 0, 42 ); // 42
foo( 5 ); // 36
foo( 5, undefined ); // 36 <-- `undefined`是缺失
foo( 5, null ); // 5 <-- null強制轉換為`0`
foo( undefined, 6 ); // 17 <-- `undefined`是缺失
foo( null, 6 ); // 6 <-- null強制轉換為`0`
```
注意這些結果,和它們如何暗示了與前面的方式的微妙區別和相似之處。
與常見得多的`x || 11`慣用法相比,在一個函數聲明中的`x = 11`更像`x !== undefined ? x : 11`,所以在將你的前ES6代碼轉換為這種ES6默認參數值語法時要多加小心。
注意:?一個剩余/收集參數(參見“擴散/剩余”)不能擁有默認值。所以,雖然`function foo(...vals=[1,2,3]) {`看起來是一種迷人的能力,但它不是合法的語法。有必要的話你需要繼續手動實施那種邏輯。
### 默認值表達式
函數默認值可以比像`31`這樣的簡單值復雜得多;它們可以是任何合法的表達式,甚至是函數調用:
```source-js
function bar(val) {
console.log( "bar called!" );
return y + val;
}
function foo(x = y + 3, z = bar( x )) {
console.log( x, z );
}
var y = 5;
foo(); // "bar called"
// 8 13
foo( 10 ); // "bar called"
// 10 15
y = 6;
foo( undefined, 10 ); // 9 10
```
如你所見,默認值表達式是被懶惰地求值的,這意味著他們僅在被需要時運行 —— 也就是,當一個參數的參數值被省略或者為`undefined`。
這是一個微妙的細節,但是在一個函數聲明中的正式參數是在它們自己的作用域中的(將它想象為一個僅僅圍繞在函數聲明的`(..)`外面的一個作用域氣泡),不是在函數體的作用域中。這意味著在一個默認值表達式中的標識符引用會在首先在正式參數的作用域中查找標識符,然后再查找一個外部作用域。更多信息參見本系列的?*作用域與閉包*。
考慮如下代碼:
```source-js
var w = 1, z = 2;
function foo( x = w + 1, y = x + 1, z = z + 1 ) {
console.log( x, y, z );
}
foo(); // ReferenceError
```
在默認值表達式`w + 1`中的`w`在正式參數作用域中查找`w`,但沒有找到,所以外部作用域的`w`被使用了。接下來,在默認值表達式`x + 1`中的`x`在正式參數的作用域中找到了`x`,而且走運的是`x`已經被初始化了,所以對`y`的賦值工作的很好。
然而,`z + 1`中的`z`找到了一個在那個時刻還沒有被初始化的參數變量`z`,所以它絕不會試著在外部作用域中尋找`z`。
正如我們在本章早先的“`let`聲明”一節中提到過的那樣,ES6擁有一個TDZ,它會防止一個變量在它還沒有被初始化的狀態下被訪問。因此,`z + 1`默認值表達式拋出一個TDZ`ReferenceError`錯誤。
雖然對于代碼的清晰度來說不見得是一個好主意,一個默認值表達式甚至可以是一個內聯的函數表達式調用 —— 通常被稱為一個立即被調用的函數表達式(IIFE):
```source-js
function foo( x =
(function(v){ return v + 11; })( 31 )
) {
console.log( x );
}
foo(); // 42
```
一個IIFE(或者任何其他被執行的內聯函數表達式)作為默認值表示來說很合適是非常少見的。如果你發現自己試圖這么做,那么就退一步再考慮一下!
警告:?如果一個IIFE試圖訪問標識符`x`,而且還沒有聲明自己的`x`,那么這也將是一個TDZ錯誤,就像我們剛才討論的一樣。
前一個代碼段的默認值表達式是一個IIFE,這是因為它是通過`(31)`在內聯時立即被執行。如果我們去掉這一部分,賦予`x`的默認值將會僅僅是一個函數的引用,也許像一個默認的回調。可能有一些情況這種模式將十分有用,比如:
```source-js
function ajax(url, cb = function(){}) {
// ..
}
ajax( "http://some.url.1" );
```
這種情況下,我們實質上想在沒有其他值被指定時,讓默認的`cb`是一個沒有操作的空函數。這個函數表達式只是一個函數引用,不是一個調用它自己(在它末尾沒有調用的`()`)以達成自己目的的函數。
從JS的早些年開始,就有一個少為人知但是十分有用的奇怪之處可供我們使用:`Function.prototype`本身就是一個沒有操作的空函數。這樣,這個聲明可以是`cb = Function.prototype`而省去內聯函數表達式的創建。
## 解構
ES6引入了一個稱為?*解構*?的新語法特性,如果你將它考慮為?*結構化賦值*?那么它令人困惑的程度可能會小一些。為了理解它的含義,考慮如下代碼:
```source-js
function foo() {
return [1,2,3];
}
var tmp = foo(),
a = tmp[0], b = tmp[1], c = tmp[2];
console.log( a, b, c ); // 1 2 3
```
如你所見,我們創建了一個手動賦值:從`foo()`返回的數組中的值到個別的變量`a`,`b`,和`c`,而且這么做我們就(不幸地)需要`tmp`變量。
相似地,我們也可以用對象這么做:
```source-js
function bar() {
return {
x: 4,
y: 5,
z: 6
};
}
var tmp = bar(),
x = tmp.x, y = tmp.y, z = tmp.z;
console.log( x, y, z ); // 4 5 6
```
屬性值`tmp.x`被賦值給變量`x`,`tmp.y`到`y`和`tmp.z`到`z`也一樣。
從一個數組中取得索引的值,或從一個對象中取得屬性并手動賦值可以被認為是?*結構化賦值*。ES6為?*解構*?增加了一種專門的語法,具體地稱為?*數組解構*?和?*對象結構*。這種語法消滅了前一個代碼段中對變量`tmp`的需要,使它們更加干凈。考慮如下代碼:
```source-js
var [ a, b, c ] = foo();
var { x: x, y: y, z: z } = bar();
console.log( a, b, c ); // 1 2 3
console.log( x, y, z ); // 4 5 6
```
你很可能更加習慣于看到像`[a,b,c]`這樣的東西出現在一個`=`賦值的右手邊的語法,即作為要被賦予的值。
解構對稱地翻轉了這個模式,所以在`=`賦值左手邊的`[a,b,c]`被看作是為了將右手邊的數組拆解為分離的變量賦值的某種“模式”。
類似地,`{ x: x, y: y, z: z }`指明了一種“模式”把來自于`bar()`的對象拆解為分離的變量賦值。
### 對象屬性賦值模式
讓我們深入前一個代碼段中的`{ x: x, .. }`語法。如果屬性名與你想要聲明的變量名一致,你實際上可以縮寫這個語法:
```source-js
var { x, y, z } = bar();
console.log( x, y, z ); // 4 5 6
```
很酷,對吧?
但`{ x, .. }`是省略了`x:`部分還是省略了`: x`部分?當我們使用這種縮寫語法時,我們實際上省略了`x:`部分。這看起來可能不是一個重要的細節,但是一會兒你就會了解它的重要性。
如果你能寫縮寫形式,那為什么你還要寫出更長的形式呢?因為更長的形式事實上允許你將一個屬性賦值給一個不同的變量名稱,這有時很有用:
```source-js
var { x: bam, y: baz, z: bap } = bar();
console.log( bam, baz, bap ); // 4 5 6
console.log( x, y, z ); // ReferenceError
```
關于這種對象結構形式有一個微妙但超級重要的怪異之處需要理解。為了展示為什么它可能是一個你需要注意的坑,讓我們考慮一下普通對象字面量的“模式”是如何被指定的:
```source-js
var X = 10, Y = 20;
var o = { a: X, b: Y };
console.log( o.a, o.b ); // 10 20
```
在`{ a: X, b: Y }`中,我們知道`a`是對象屬性,而`X`是被賦值給它的源值。換句話說,它的語義模式是`目標: 源`,或者更明顯地,`屬性別名: 值`。我們能直觀地明白這一點,因為它和`=`賦值是一樣的,而它的模式就是`目標 = 源`。
然而,當你使用對象解構賦值時 —— 也就是,將看起來像是對象字面量的`{ .. }`語法放在`=`操作符的左手邊 —— 你反轉了這個`目標: 源`的模式。
回想一下:
```source-js
var { x: bam, y: baz, z: bap } = bar();
```
這里面對稱的模式是`源: 目標`(或者`值: 屬性別名`)。`x: bam`意味著屬性`x`是源值而`bam`是被賦值的目標變量。換句話說,對象字面量是`target <-- source`,而對象解構賦值是`source --> target`。看到它是如何反轉的了嗎?
有另外一種考慮這種語法的方式,可能有助于緩和這種困惑。考慮如下代碼:
```source-js
var aa = 10, bb = 20;
var o = { x: aa, y: bb };
var { x: AA, y: BB } = o;
console.log( AA, BB ); // 10 20
```
在`{ x: aa, y: bb }`這一行中,`x`和`y`代表對象屬性。在`{ x: AA, y: BB }`這一行,`x`和`y`?*也*?代表對象屬性。
還記得剛才我是如何斷言`{ x, .. }`省去了`x:`部分的嗎?在這兩行中,如果你在代碼段中擦掉`x:`和`y:`部分,僅留下`aa, bb`和`AA, BB`,它的效果 —— 從概念上講,實際上不能 —— 將是從`aa`賦值到`AA`和從`bb`賦值到`BB`。
所以,這種平行性也許有助于解釋為什么對于這種ES6特性,語法模式被故意地反轉了。
*注意:*?對于解構賦值來說我更喜歡它的語法是`{ AA: x , BB: y }`,因為那樣的話可以在兩種用法中一致地使用我們更熟悉的`target: source`模式。唉,我已經被迫訓練自己的大腦去習慣這種反轉了,就像一些讀者也不得不去做的那樣。
### 不僅是聲明
至此,我們一直將解構賦值與`var`聲明(當然,它們也可以使用`let`和`const`)一起使用,但是解構是一種一般意義上的賦值操作,不僅是一種聲明。
考慮如下代碼:
```source-js
var a, b, c, x, y, z;
[a,b,c] = foo();
( { x, y, z } = bar() );
console.log( a, b, c ); // 1 2 3
console.log( x, y, z ); // 4 5 6
```
變量可以是已經被定義好的,然后解構僅僅負責賦值,正如我們已經看到的那樣。
注意:?特別對于對象解構形式來說,當我們省略了`var`/`let`/`const`聲明符時,就必須將整個賦值表達式包含在`()`中,因為如果不這樣做的話左手邊作為語句第一個元素的`{ .. }`將被視為一個語句塊兒而不是一個對象。
事實上,變量表達式(`a`,`y`,等等)不必是一個變量標識符。任何合法的賦值表達式都是允許的。例如:
```source-js
var o = {};
[o.a, o.b, o.c] = foo();
( { x: o.x, y: o.y, z: o.z } = bar() );
console.log( o.a, o.b, o.c ); // 1 2 3
console.log( o.x, o.y, o.z ); // 4 5 6
```
你甚至可以在解構中使用計算型屬性名。考慮如下代碼:
```source-js
var which = "x",
o = {};
( { [which]: o[which] } = bar() );
console.log( o.x ); // 4
```
`[which]:`的部分是計算型屬性名,它的結果是`x`?—— 將從當前的對象中拆解出來作為賦值的源頭的屬性。`o[which]`的部分只是一個普通的對象鍵引用,作為賦值的目標來說它與`o.x`是等價的。
你可以使用普通的賦值來創建對象映射/變形,例如:
```source-js
var o1 = { a: 1, b: 2, c: 3 },
o2 = {};
( { a: o2.x, b: o2.y, c: o2.z } = o1 );
console.log( o2.x, o2.y, o2.z ); // 1 2 3
```
或者你可以將對象映射進一個數組,例如:
```source-js
var o1 = { a: 1, b: 2, c: 3 },
a2 = [];
( { a: a2[0], b: a2[1], c: a2[2] } = o1 );
console.log( a2 ); // [1,2,3]
```
或者從另一個方向:
```source-js
var a1 = [ 1, 2, 3 ],
o2 = {};
[ o2.a, o2.b, o2.c ] = a1;
console.log( o2.a, o2.b, o2.c ); // 1 2 3
```
或者你可以將一個數組重排到另一個數組中:
```source-js
var a1 = [ 1, 2, 3 ],
a2 = [];
[ a2[2], a2[0], a2[1] ] = a1;
console.log( a2 ); // [2,3,1]
```
你甚至可以不使用臨時變量來解決傳統的“交換兩個變量”的問題:
```source-js
var x = 10, y = 20;
[ y, x ] = [ x, y ];
console.log( x, y ); // 20 10
```
警告:?小心:你不應該將聲明和賦值混在一起,除非你想要所有的賦值表達式?*也*?被視為聲明。否則,你會得到一個語法錯誤。這就是為什么在剛才的例子中我必須將`var a2 = []`與`[ a2[0], .. ] = ..`解構賦值分開做。嘗試`var [ a2[0], .. ] = ..`沒有任何意義,因為`a2[0]`不是一個合法的聲明標識符;很顯然它也不能隱含地創建一個`var a2 = []`聲明來使用。
### 重復賦值
對象解構形式允許源屬性(持有任意值的類型)被羅列多次。例如:
```source-js
var { a: X, a: Y } = { a: 1 };
X; // 1
Y; // 1
```
這意味著你既可以解構一個子對象/數組屬性,也可以捕獲這個子對象/數組的值本身。考慮如下代碼:
```source-js
var { a: { x: X, x: Y }, a } = { a: { x: 1 } };
X; // 1
Y; // 1
a; // { x: 1 }
( { a: X, a: Y, a: [ Z ] } = { a: [ 1 ] } );
X.push( 2 );
Y[0] = 10;
X; // [10,2]
Y; // [10,2]
Z; // 1
```
關于解構有一句話要提醒:像我們到目前為止的討論中做的那樣,將所有的解構賦值都羅列在單獨一行中的方式可能很誘人。然而,一個好得多的主意是使用恰當的縮進將解構賦值的模式分散在多行中 —— 和你在JSON或對象字面量中做的事非常相似 —— 為了可讀性。
```source-js
// 很難讀懂:
var { a: { b: [ c, d ], e: { f } }, g } = obj;
// 好一些:
var {
a: {
b: [ c, d ],
e: { f }
},
g
} = obj;
```
記住:解構的目的不僅是為了少打些字,更多是為了聲明可讀性
####解構賦值表達式
帶有對象或數組解構的賦值表達式的完成值是右手邊完整的對象/數組值。考慮如下代碼:
```source-js
var o = { a:1, b:2, c:3 },
a, b, c, p;
p = { a, b, c } = o;
console.log( a, b, c ); // 1 2 3
p === o; // true
```
在前面的代碼段中,`p`被賦值為對象`o`的引用,而不是`a`,`b`,或`c`的值。數組解構也是一樣:
```source-js
var o = [1,2,3],
a, b, c, p;
p = [ a, b, c ] = o;
console.log( a, b, c ); // 1 2 3
p === o; // true
```
通過將這個對象/數組作為完成值傳遞下去,你可將解構賦值表達式鏈接在一起:
```source-js
var o = { a:1, b:2, c:3 },
p = [4,5,6],
a, b, c, x, y, z;
( {a} = {b,c} = o );
[x,y] = [z] = p;
console.log( a, b, c ); // 1 2 3
console.log( x, y, z ); // 4 5 4
```
### 太多,太少,正合適
對于數組解構賦值和對象解構賦值兩者來說,你不必分配所有出現的值。例如:
```source-js
var [,b] = foo();
var { x, z } = bar();
console.log( b, x, z ); // 2 4 6
```
從`foo()`返回的值`1`和`3`被丟棄了,從`bar()`返回的值`5`也是。
相似地,如果你試著分配比你正在解構/拆解的值要多的值時,它們會如你所想的那樣安靜地退回到`undefined`:
```source-js
var [,,c,d] = foo();
var { w, z } = bar();
console.log( c, z ); // 3 6
console.log( d, w ); // undefined undefined
```
這種行為平行地遵循早先提到的“`undefined`意味著缺失”原則。
我們在本章早先檢視了`...`操作符,并看到了它有時可以用于將一個數組值擴散為它的分離值,而有時它可以被用于相反的操作:將一組值收集進一個數組。
除了在函數聲明中的收集/剩余用法以外,`...`可以在解構賦值中實施相同的行為。為了展示這一點,讓我們回想一下本章早先的一個代碼段:
```source-js
var a = [2,3,4];
var b = [ 1, ...a, 5 ];
console.log( b ); // [1,2,3,4,5]
```
我們在這里看到因為`...a`出現在數組`[ .. ]`中值的位置,所以它將`a`擴散開。如果`...a`出現一個數組解構的位置,它會實施收集行為:
```source-js
var a = [2,3,4];
var [ b, ...c ] = a;
console.log( b, c ); // 2 [3,4]
```
解構賦值`var [ .. ] = a`為了將`a`賦值給在`[ .. ]`中描述的模式而將它擴散開。第一部分的名稱`b`對應`a`中的第一個值(`2`)。然后`...c`將剩余的值(`3`和`4`)收集到一個稱為`c`的數組中。
注意:?我們已經看到`...`是如何與數組一起工作的,但是對象呢?那不是一個ES6特性,但是參看第八章中關于一種可能的“ES6之后”的特性的討論,它可以讓`...`擴散或者收集對象。
### 默認值賦值
兩種形式的解構都可以為賦值提供默認值選項,它使用和早先討論過的默認函數參數值相似的`=`語法。
考慮如下代碼:
```source-js
var [ a = 3, b = 6, c = 9, d = 12 ] = foo();
var { x = 5, y = 10, z = 15, w = 20 } = bar();
console.log( a, b, c, d ); // 1 2 3 12
console.log( x, y, z, w ); // 4 5 6 20
```
你可以將默認值賦值與前面講過的賦值表達式語法組合在一起。例如:
```source-js
var { x, y, z, w: WW = 20 } = bar();
console.log( x, y, z, WW ); // 4 5 6 20
```
如果你在一個解構中使用一個對象或者數組作為默認值,那么要小心不要把自己(或者讀你的代碼的其他開發者)搞糊涂了。你可能會創建一些非常難理解的代碼:
```source-js
var x = 200, y = 300, z = 100;
var o1 = { x: { y: 42 }, z: { y: z } };
( { y: x = { y: y } } = o1 );
( { z: y = { y: z } } = o1 );
( { x: z = { y: x } } = o1 );
```
你能從這個代碼段中看出`x`,`y`和`z`最終是什么值嗎?花點兒時間好好考慮一下,我能想象你的樣子。我會終結這個懸念:
```source-js
console.log( x.y, y.y, z.y ); // 300 100 42
```
這里的要點是:解構很棒也可以很有用,但是如果使用得不明智,它也是一把可以傷人(某人的大腦)的利劍。
### 嵌套解構
如果你正在解構的值擁有嵌套的對象或數組,你也可以解構這些嵌套的值:
```source-js
var a1 = [ 1, [2, 3, 4], 5 ];
var o1 = { x: { y: { z: 6 } } };
var [ a, [ b, c, d ], e ] = a1;
var { x: { y: { z: w } } } = o1;
console.log( a, b, c, d, e ); // 1 2 3 4 5
console.log( w ); // 6
```
嵌套的解構可以是一種將對象名稱空間扁平化的簡單方法。例如:
```source-js
var App = {
model: {
User: function(){ .. }
}
};
// 取代:
// var User = App.model.User;
var { model: { User } } = App;
```
### 參數解構
你能在下面的代碼段中發現賦值嗎?
```source-js
function foo(x) {
console.log( x );
}
foo( 42 );
```
其中的賦值有點兒被隱藏的感覺:當`foo(42)`被執行時`42`(參數值)被賦值給`x`(參數)。如果參數/參數值對是一種賦值,那么按常理說它是一個可以被解構的賦值,對吧?當然!
考慮參數的數組解構:
```source-js
function foo( [ x, y ] ) {
console.log( x, y );
}
foo( [ 1, 2 ] ); // 1 2
foo( [ 1 ] ); // 1 undefined
foo( [] ); // undefined undefined
```
參數也可以進行對象解構:
```source-js
function foo( { x, y } ) {
console.log( x, y );
}
foo( { y: 1, x: 2 } ); // 2 1
foo( { y: 42 } ); // undefined 42
foo( {} ); // undefined undefined
```
這種技術是命名參數值(一個長期以來被渴求的JS特性!)的一種近似解法:對象上的屬性映射到被解構的同名參數上。這也意味著我們免費地(在任何位置)得到了可選參數,如你所見,省去“參數”`x`可以如我們期望的那樣工作。
當然,先前討論過的所有解構的種類對于參數解構來說都是可用的,包括嵌套解構,默認值,和其他。解構也可以和其他ES6函數參數功能很好地混合在一起,比如默認參數值和剩余/收集參數。
考慮這些快速的示例(當然這沒有窮盡所有可能的種類):
```source-js
function f1([ x=2, y=3, z ]) { .. }
function f2([ x, y, ...z], w) { .. }
function f3([ x, y, ...z], ...w) { .. }
function f4({ x: X, y }) { .. }
function f5({ x: X = 10, y = 20 }) { .. }
function f6({ x = 10 } = {}, { y } = { y: 10 }) { .. }
```
為了展示一下,讓我們從這個代碼段中取一個例子來檢視:
```source-js
function f3([ x, y, ...z], ...w) {
console.log( x, y, z, w );
}
f3( [] ); // undefined undefined [] []
f3( [1,2,3,4], 5, 6 ); // 1 2 [3,4] [5,6]
```
這里使用了兩個`...`操作符,他們都是將值收集到數組中(`z`和`w`),雖然`...z`是從第一個數組參數值的剩余值中收集,而`...w`是從第一個之后的剩余主參數值中收集的。
#### 解構默認值 + 參數默認值
有一個微妙的地方你應當注意要特別小心 —— 解構默認值與函數參數默認值的行為之間的不同。例如:
```source-js
function f6({ x = 10 } = {}, { y } = { y: 10 }) {
console.log( x, y );
}
f6(); // 10 10
```
首先,看起來我們用兩種不同的方法為參數`x`和`y`都聲明了默認值`10`。然而,這兩種不同的方式會在特定的情況下表現出不同的行為,而且這種區別極其微妙。
考慮如下代碼:
```source-js
f6( {}, {} ); // 10 undefined
```
等等,為什么會這樣?十分清楚,如果在第一個參數值的對象中沒有一個同名屬性被傳遞,那么命名參數`x`將默認為`10`。
但`y`是`undefined`是怎么回事兒?值`{ y: 10 }`是一個作為函數參數默認值的對象,不是結構默認值。因此,它僅在第二個參數根本沒有被傳遞,或者`undefined`被傳遞時生效,
在前面的代碼段中,我們傳遞了第二個參數(`{}`),所以默認值`{ y: 10 }`不被使用,而解構`{ y }`會針對被傳入的空對象值`{}`發生。
現在,將`{ y } = { y: 10 }`與`{ x = 10 } = {}`比較一下。
對于`x`的使用形式來說,如果第一個函數參數值被省略或者是`undefined`,會默認地使用空對象`{}`。然后,不管在第一個參數值的位置上是什么值 —— 要么是默認的`{}`,要么是你傳入的 —— 都會被`{ x = 10 }`解構,它會檢查屬性`x`是否被找到,如果沒有找到(或者是`undefined`),默認值`10`會被設置到命名參數`x`上。
深呼吸。回過頭去把最后幾段多讀幾遍。讓我們用代碼復習一下:
```source-js
function f6({ x = 10 } = {}, { y } = { y: 10 }) {
console.log( x, y );
}
f6(); // 10 10
f6( undefined, undefined ); // 10 10
f6( {}, undefined ); // 10 10
f6( {}, {} ); // 10 undefined
f6( undefined, {} ); // 10 undefined
f6( { x: 2 }, { y: 3 } ); // 2 3
```
一般來說,與參數`y`的默認行為比起來,參數`x`的默認行為可能看起來更可取也更合理。因此,理解`{ x = 10 } = {}`形式與`{ y } = { y: 10 }`形式為何與如何不同是很重要的。
如果這仍然有點兒模糊,回頭再把它讀一遍,并親自把它玩弄一番。未來的你將會感謝你花了時間把這種非常微妙的,晦澀的細節的坑搞明白。
#### 嵌套默認值:解構與重構
雖然一開始可能很難掌握,但是為一個嵌套的對象的屬性設置默認值產生了一種有趣的慣用法:將對象解構與一種我稱為?*重構*的東西一起使用。
考慮在一個嵌套的對象結構中的一組默認值,就像下面這樣:
```source-js
// 摘自:http://es-discourse.com/t/partial-default-arguments/120/7
var defaults = {
options: {
remove: true,
enable: false,
instance: {}
},
log: {
warn: true,
error: true
}
};
```
現在,我們假定你有一個稱為`config`的對象,它有一些這其中的值,但也許不全有,而且你想要將所有的默認值設置到這個對象的缺失點上,但不覆蓋已經存在的特定設置:
```source-js
var config = {
options: {
remove: false,
instance: null
}
};
```
你當然可以手動這樣做,就像你可能曾經做過的那樣:
```source-js
config.options = config.options || {};
config.options.remove = (config.options.remove !== undefined) ?
config.options.remove : defaults.options.remove;
config.options.enable = (config.options.enable !== undefined) ?
config.options.enable : defaults.options.enable;
...
```
討厭。
另一些人可能喜歡用覆蓋賦值的方式來完成這個任務。你可能會被ES6的`Object.assign(..)`工具(見第六章)所吸引,來首先克隆`defaults`中的屬性然后使用從`config`中克隆的屬性覆蓋它,像這樣:
```source-js
config = Object.assign( {}, defaults, config );
```
這看起來好多了,是吧?但是這里有一個重大問題!`Object.assign(..)`是淺拷貝,這意味著當它拷貝`defaults.options`時,它僅僅拷貝這個對象的引用,而不是深度克隆這個對象的屬性到一個`config.options`對象。`Object.assign(..)`需要在你的對象樹的每一層中實施才能得到你期望的深度克隆。
注意:?許多JS工具庫/框架都為對象的深度克隆提供它們自己的選項,但是那些方式和它們的坑超出了我們在這里的討論范圍。
那么讓我們檢視一下ES6的帶有默認值的對象解構能否幫到我們:
```source-js
config.options = config.options || {};
config.log = config.log || {};
({
options: {
remove: config.options.remove = defaults.options.remove,
enable: config.options.enable = defaults.options.enable,
instance: config.options.instance = defaults.options.instance
} = {},
log: {
warn: config.log.warn = defaults.log.warn,
error: config.log.error = defaults.log.error
} = {}
} = config);
```
不像`Object.assign(..)`的虛假諾言(因為它只是淺拷貝)那么好,但是我想它要比手動的方式強多了。雖然它仍然很不幸地帶有冗余和重復。
前面的代碼段的方式可以工作,因為我黑進了結構和默認機制來為我做屬性的`=== undefined`檢查和賦值的決定。這里的技巧是,我解構了`config`(看看在代碼段末尾的`= config`),但是我將所有解構出來的值又立即賦值回`config`,帶著`config.options.enable`賦值引用。
但還是太多了。讓我們看看能否做得更好。
下面的技巧在你知道你正在解構的所有屬性的名稱都是唯一的情況下工作得最好。但即使不是這樣的情況你也仍然可以使用它,只是沒有那么好 —— 你將不得不分階段解構,或者創建獨一無二的本地變量作為臨時的別名。
如果我們將所有的屬性完全解構為頂層變量,那么我們就可以立即重構來重組原本的嵌套對象解構。
但是所有那些游蕩在外的臨時變量將會污染作用域。所以,讓我們通過一個普通的`{ }`包圍塊兒來使用塊兒作用域(參見本章早先的“塊兒作用域聲明”)。
```source-js
// 將`defaults`混入`config`
{
// 解構(使用默認值賦值)
let {
options: {
remove = defaults.options.remove,
enable = defaults.options.enable,
instance = defaults.options.instance
} = {},
log: {
warn = defaults.log.warn,
error = defaults.log.error
} = {}
} = config;
// 重構
config = {
options: { remove, enable, instance },
log: { warn, error }
};
}
```
這看起來好多了,是吧?
注意:?你也可以使用箭頭IIFE來代替一般的`{ }`塊兒和`let`聲明來達到圈占作用域的目的。你的解構賦值/默認值將位于參數列表中,而你的重構將位于函數體的`return`語句中。
在重構部分的`{ warn, error }`語法可能是你初次見到;它稱為“簡約屬性”,我們將在下一節講解它!
## 對象字面量擴展
ES6給不起眼兒的`{ .. }`對象字面量增加了幾個重要的便利擴展。
### 簡約屬性
你一定很熟悉用這種形式的對象字面量聲明:
```source-js
var x = 2, y = 3,
o = {
x: x,
y: y
};
```
如果到處說`x: x`總是讓你感到繁冗,那么有個好消息。如果你需要定義一個名稱和詞法標識符一致的屬性,你可以將它從`x: x`縮寫為`x`。考慮如下代碼:
```source-js
var x = 2, y = 3,
o = {
x,
y
};
```
### 簡約方法
本著與我們剛剛檢視的簡約屬性相同的精神,添附在對象字面量屬性上的函數也有一種便利簡約形式。
以前的方式:
```source-js
var o = {
x: function(){
// ..
},
y: function(){
// ..
}
}
```
而在ES6中:
```source-js
var o = {
x() {
// ..
},
y() {
// ..
}
}
```
警告:?雖然`x() { .. }`看起來只是`x: function(){ .. }`的縮寫,但是簡約方法有一種特殊行為,是它們對應的老方式所不具有的;確切地說,是允許`super`(參見本章稍后的“對象`super`”)的使用。
Generator(見第四章)也有一種簡約方法形式:
```source-js
var o = {
*foo() { .. }
};
```
#### 簡約匿名
雖然這種便利縮寫十分誘人,但是這其中有一個微妙的坑要小心。為了展示這一點,讓我們檢視一下如下的前ES6代碼,你可能會試著使用簡約方法來重構它:
```source-js
function runSomething(o) {
var x = Math.random(),
y = Math.random();
return o.something( x, y );
}
runSomething( {
something: function something(x,y) {
if (x > y) {
// 使用相互對調的`x`和`y`來遞歸地調用
return something( y, x );
}
return y - x;
}
} );
```
這段蠢代碼只是生成兩個隨機數,然后用大的減去小的。但這里重要的不是它做的是什么,而是它是如何被定義的。讓我把焦點放在對象字面量和函數定義上,就像我們在這里看到的:
```source-js
runSomething( {
something: function something(x,y) {
// ..
}
} );
```
為什么我們同時說`something:`和`function something`?這不是冗余嗎?實際上,不是,它們倆被用于不同的目的。屬性`something`讓我們能夠調用`o.something(..)`,有點兒像它的公有名稱。但是第二個`something`是一個詞法名稱,使這個函數可以為了遞歸而從內部引用它自己。
你能看出來為什么`return something(y,x)`這一行需要名稱`something`來引用這個函數嗎?因為這里沒有對象的詞法名稱,要是有的話我們就可以說`return o.something(y,x)`或者其他類似的東西。
當一個對象字面量的確擁有一個標識符名稱時,這其實是一個很常見的做法,比如:
```source-js
var controller = {
makeRequest: function(..){
// ..
controller.makeRequest(..);
}
};
```
這是個好主意嗎?也許是,也許不是。你在假設名稱`controller`將總是指向目標對象。但它也很可能不是 —— 函數`makeRequest(..)`不能控制外部的代碼,因此不能強制你的假設一定成立。這可能會回過頭來咬到你。
另一些人喜歡使用`this`定義這樣的東西:
```source-js
var controller = {
makeRequest: function(..){
// ..
this.makeRequest(..);
}
};
```
這看起來不錯,而且如果你總是用`controller.makeRequest(..)`來調用方法的話它就應該能工作。但現在你有一個`this`綁定的坑,如果你做這樣的事情的話:
```source-js
btn.addEventListener( "click", controller.makeRequest, false );
```
當然,你可以通過傳遞`controller.makeRequest.bind(controller)`作為綁定到事件上的處理器引用來解決這個問題。但是這很討厭 —— 它不是很吸引人。
或者要是你的內部`this.makeRequest(..)`調用需要從一個嵌套的函數內發起呢?你會有另一個`this`綁定災難,人們經常使用`var self = this`這種用黑科技解決,就像:
```source-js
var controller = {
makeRequest: function(..){
var self = this;
btn.addEventListener( "click", function(){
// ..
self.makeRequest(..);
}, false );
}
};
```
更討厭。
注意:?更多關于`this`綁定規則和陷阱的信息,參見本系列的?*this與對象原型*?的第一到二章。
好了,這些與簡約方法有什么關系?回想一下我們的`something(..)`方法定義:
```source-js
runSomething( {
something: function something(x,y) {
// ..
}
} );
```
在這里的第二個`something`提供了一個超級便利的詞法標識符,它總是指向函數自己,給了我們一個可用于遞歸,事件綁定/解除等等的完美引用 —— 不用亂搞`this`或者使用不可靠的對象引用。
太好了!
那么,現在我們試著將函數引用重構為這種ES6解約方法的形式:
```source-js
runSomething( {
something(x,y) {
if (x > y) {
return something( y, x );
}
return y - x;
}
} );
```
第一眼看上去不錯,除了這個代碼將會壞掉。`return something(..)`調用經不會找到`something`標識符,所以你會得到一個`ReferenceError`。噢,但為什么?
上面的ES6代碼段將會被翻譯為:
```source-js
runSomething( {
something: function(x,y){
if (x > y) {
return something( y, x );
}
return y - x;
}
} );
```
仔細看。你看出問題了嗎?簡約方法定義暗指`something: function(x,y)`。看到我們依靠的第二個`something`是如何被省略的了嗎?換句話說,簡約方法暗指匿名函數表達式。
對,討厭。
注意:?你可能認為在這里`=>`箭頭函數是一個好的解決方案。但是它們也同樣不夠,因為它們也是匿名函數表達式。我們將在本章稍后的“箭頭函數”中講解它們。
一個部分地補償了這一點的消息是,我們的簡約函數`something(x,y)`將不會是完全匿名的。參見第七章的“函數名”來了解ES6函數名稱的推斷規則。這不會在遞歸中幫到我們,但是它至少在調試時有用處。
那么我們怎樣總結簡約方法?它們簡短又甜蜜,而且很方便。但是你應當僅在你永遠不需要將它們用于遞歸或事件綁定/解除時使用它們。否則,就堅持使用你的老式`something: function something(..)`方法定義。
你的很多方法都將可能從簡約方法定義中受益,這是個非常好的消息!只要小心幾處未命名的災難就好。
#### ES5 Getter/Setter
技術上講,ES5定義了getter/setter字面形式,但是看起來它們沒有被太多地使用,這主要是由于缺乏轉譯器來處理這種新的語法(其實,它是ES5中加入的唯一的主要新語法)。所以雖然它不是一個ES6的新特性,我們也將簡單地復習一下這種形式,因為它可能會隨著ES6的向前發展而變得有用得多。
考慮如下代碼:
```source-js
var o = {
__id: 10,
get id() { return this.__id++; },
set id(v) { this.__id = v; }
}
o.id; // 10
o.id; // 11
o.id = 20;
o.id; // 20
// 而:
o.__id; // 21
o.__id; // 還是 —— 21!
```
這些getter和setter字面形式也可以出現在類中;參見第三章。
警告:?可能不太明顯,但是setter字面量必須恰好有一個被聲明的參數;省略它或羅列其他的參數都是不合法的語法。這個單獨的必須參數?*可以*?使用解構和默認值(例如,`set id({ id: v = 0 }) { .. }`),但是收集/剩余`...`是不允許的(`set id(...v) { .. }`)。
### 計算型屬性名
你可能曾經遇到過像下面的代碼段那樣的情況,你的一個或多個屬性名來自于某種表達式,因此你不能將它們放在對象字面量中:
```source-js
var prefix = "user_";
var o = {
baz: function(..){ .. }
};
o[ prefix + "foo" ] = function(..){ .. };
o[ prefix + "bar" ] = function(..){ .. };
..
```
ES6為對象字面定義增加了一種語法,它允許你指定一個應當被計算的表達式,其結果就是被賦值屬性名。考慮如下代碼:
```source-js
var prefix = "user_";
var o = {
baz: function(..){ .. },
[ prefix + "foo" ]: function(..){ .. },
[ prefix + "bar" ]: function(..){ .. }
..
};
```
任何合法的表達式都可以出現在位于對象字面定義的屬性名位置的`[ .. ]`內部。
很有可能,計算型屬性名最經常與`Symbol`(我們將在本章稍后的“Symbol”中講解)一起使用,比如:
```source-js
var o = {
[Symbol.toStringTag]: "really cool thing",
..
};
```
`Symbol.toStringTag`是一個特殊的內建值,我們使用`[ .. ]`語法求值得到,所以我們可以將值`"really cool thing"`賦值給這個特殊的屬性名。
計算型屬性名還可以作為簡約方法或簡約generator的名稱出現:
```source-js
var o = {
["f" + "oo"]() { .. } // 計算型簡約方法
*["b" + "ar"]() { .. } // 計算型簡約generator
};
```
### `[[Prototype]]`
我們不會在這里講解原型的細節,所以關于它的更多信息,參見本系列的?*this與對象原型*。
有時候在你聲明對象字面量的同時給它的`[[Prototype]]`賦值很有用。下面的代碼在一段時期內曾經是許多JS引擎的一種非標準擴展,但是在ES6中得到了標準化:
```source-js
var o1 = {
// ..
};
var o2 = {
__proto__: o1,
// ..
};
```
`o2`是用一個對象字面量聲明的,但它也被`[[Prototype]]`鏈接到了`o1`。這里的`__proto__`屬性名還可以是一個字符串`"__proto__"`,但是要注意它?*不能*?是一個計算型屬性名的結果(參見前一節)。
客氣點兒說,`__proto__`是有爭議的。在ES6中,它看起來是一個最終被很勉強地標準化了的,幾十年前的自主擴展功能。實際上,它屬于ES6的“Annex B”,這一部分羅列了JS感覺它僅僅為了兼容性的原因,而不得不標準化的東西。
警告:?雖然我勉強贊同在一個對象字面定義中將`__proto__`作為一個鍵,但我絕對不贊同在對象屬性形式中使用它,就像`o.__proto__`。這種形式既是一個getter也是一個setter(同樣也是為了兼容性的原因),但絕對存在更好的選擇。更多信息參見本系列的?*this與對象原型*。
對于給一個既存的對象設置`[[Prototype]]`,你可以使用ES6的工具`Object.setPrototypeOf(..)`。考慮如下代碼:
```source-js
var o1 = {
// ..
};
var o2 = {
// ..
};
Object.setPrototypeOf( o2, o1 );
```
注意:?我們將在第六章中再次討論`Object`。“`Object.setPrototypeOf(..)`靜態函數”提供了關于`Object.setPrototypeOf(..)`的額外細節。另外參見“`Object.assign(..)`靜態函數”來了解另一種將`o2`原型關聯到`o1`的形式。
### 對象`super`
`super`通常被認為是僅與類有關。然而,由于JS對象僅有原型而沒有類的性質,`super`是同樣有效的,而且在普通對象的簡約方法中行為幾乎一樣。
考慮如下代碼:
```source-js
var o1 = {
foo() {
console.log( "o1:foo" );
}
};
var o2 = {
foo() {
super.foo();
console.log( "o2:foo" );
}
};
Object.setPrototypeOf( o2, o1 );
o2.foo(); // o1:foo
// o2:foo
```
警告:?`super`僅在簡約方法中允許使用,而不允許在普通的函數表達式屬性中。而且它還僅允許使用`super.XXX`形式(屬性/方法訪問),而不是`super()`形式。
在方法`o2.foo()`中的`super`引用被靜態地鎖定在了`o2`,而且明確地說是`o2`的`[[Prototype]]`。這里的`super`基本上是`Object.getPrototypeOf(o2)`?—— 顯然被解析為`o1`?—— 這就是他如何找到并調用`o1.foo()`的。
關于`super`的完整細節,參見第三章的“類”。
## 模板字面量
在這一節的最開始,我將不得不呼喚這個ES6特性的極其……誤導人的名稱,這要看在你的經驗中?*模板(template)*?一詞的含義是什么。
許多開發者認為模板是一段可復用的,可重繪的文本,就像大多數模板引擎(Mustache,Handlebars,等等)提供的能力那樣。ES6中使用的?*模板*?一詞暗示著相似的東西,就像一種聲明可以被重繪的內聯模板字面量的方法。然而,這根本不是考慮這個特性的正確方式。
所以,在我們繼續之前,我把它重命名為它本應被稱呼的名字:*插值型字符串字面量*(或者略稱為?*插值型字面量*)。
你已經十分清楚地知道了如何使用`"`或`'`分隔符來聲明字符串字面量,而且你還知道它們不是(像有些語言中擁有的)內容將被解析為插值表達式的?*智能字符串*。
但是,ES6引入了一種新型的字符串字面量,使用反引號```作為分隔符。這些字符串字面量允許嵌入基本的字符串插值表達式,之后這些表達式自動地被解析和求值。
這是老式的前ES6方式:
```source-js
var name = "Kyle";
var greeting = "Hello " + name + "!";
console.log( greeting ); // "Hello Kyle!"
console.log( typeof greeting ); // "string"
```
現在,考慮這種新的ES6方式:
```source-js
var name = "Kyle";
var greeting = `Hello ${name}!`;
console.log( greeting ); // "Hello Kyle!"
console.log( typeof greeting ); // "string"
```
如你所見,我們在一系列被翻譯為字符串字面量的字符周圍使用了``..``,但是`${..}`形式中的任何表達式都將立即內聯地被解析和求值。稱呼這樣的解析和求值的高大上名詞就是?*插值(interpolation)*(比模板要準確多了)。
被插值的字符串字面量表達式的結果只是一個老式的普通字符串,賦值給變量`greeting`。
警告:?`typeof greeting == "string"`展示了為什么不將這些實體考慮為特殊的模板值很重要,因為你不能將這種字面量的未求值形式賦值給某些東西并復用它。``..``字符串字面量在某種意義上更像是IIFE,因為它自動內聯地被求值。``..``字符串字面量的結果只不過是一個簡單的字符串。
插值型字符串字面量的一個真正的好處是他們允許被分割為多行:
```source-js
var text =
`Now is the time for all good men
to come to the aid of their
country!`;
console.log( text );
// Now is the time for all good men
// to come to the aid of their
// country!
```
在插值型字符串字面量中的換行將會被保留在字符串值中。
除非在字面量值中作為明確的轉義序列出現,回車字符`\r`(編碼點`U+000D`)的值或者回車+換行序列`\r\n`(編碼點`U+000D`和`U+000A`)的值都會被泛化為一個換行字符`\n`(編碼點`U+000A`)。但不要擔心;這種泛化很少見而且很可能僅會在你將文本拷貝粘貼到JS文件中時才會發生。
### 插值表達式
在一個插值型字符串字面量中,任何合法的表達式都被允許出現在`${..}`內部,包括函數調用,內聯函數表達式調用,甚至是另一個插值型字符串字面量!
考慮如下代碼:
```source-js
function upper(s) {
return s.toUpperCase();
}
var who = "reader";
var text =
`A very ${upper( "warm" )} welcome
to all of you ${upper( `${who}s` )}!`;
console.log( text );
// A very WARM welcome
// to all of you READERS!
```
當我們組合變量`who`與字符串`s`時, 相對于`who + "s"`,這里的內部插值型字符串字面量``${who}s``更方便一些。有些情況下嵌套的插值型字符串字面量是有用的,但是如果你發現自己做這樣的事情太頻繁,或者發現你自己嵌套了好幾層時,你就要小心一些。
如果確實有這樣情況,你的字符串你值生產過程很可能可以從某些抽象中獲益。
警告:?作為一個忠告,使用這樣的新發現的力量時要非常小心你代碼的可讀性。就像默認值表達式和解構賦值表達式一樣,僅僅因為你?*能*?做某些事情,并不意味著你?*應該*?做這些事情。在使用新的ES6技巧時千萬不要做過了頭,使你的代碼比你或者你的其他隊友聰明。
#### 表達式作用域
關于作用域的一個快速提醒是它用于解析表達式中的變量時。我早先提到過一個插值型字符串字面量與IIFE有些相像,事實上這也可以考慮為作用域行為的一種解釋。
考慮如下代碼:
```source-js
function foo(str) {
var name = "foo";
console.log( str );
}
function bar() {
var name = "bar";
foo( `Hello from ${name}!` );
}
var name = "global";
bar(); // "Hello from bar!"
```
在函數`bar()`內部,字符串字面量``..``被表達的那一刻,可供它查找的作用域發現變量的`name`的值為`"bar"`。既不是全局的`name`也不是`foo(..)`的`name`。換句話說,一個插值型字符串字面量在它出現的地方是詞法作用域的,而不是任何方式的動態作用域。
### 標簽型模板字面量
再次為了合理性而重命名這個特性:*標簽型字符串字面量*。
老實說,這是一個ES6提供的更酷的特性。它可能看起來有點兒奇怪,而且也許一開始看起來一般不那么實用。但一旦你花些時間在它上面,標簽型字符串字面量的用處可能會令你驚訝。
例如:
```source-js
function foo(strings, ...values) {
console.log( strings );
console.log( values );
}
var desc = "awesome";
foo`Everything is ${desc}!`;
// [ "Everything is ", "!"]
// [ "awesome" ]
```
讓我們花點兒時間考慮一下前面的代碼段中發生了什么。首先,跳出來的最刺眼的東西就是`foo`Everything...`;`。它看起來不像是任何我們曾經見過的東西。不是嗎?
它實質上是一種不需要`( .. )`的特殊函數調用。*標簽*?—— 在字符串字面量``..``之前的`foo`部分 —— 是一個應當被調用的函數的值。實際上,它可以是返回函數的任何表達式,甚至是一個返回另一個函數的函數調用,就像:
```source-js
function bar() {
return function foo(strings, ...values) {
console.log( strings );
console.log( values );
}
}
var desc = "awesome";
bar()`Everything is ${desc}!`;
// [ "Everything is ", "!"]
// [ "awesome" ]
```
但是當作為一個字符串字面量的標簽時,函數`foo(..)`被傳入了什么?
第一個參數值 —— 我們稱它為`strings`?—— 是一個所有普通字符串的數組(所有被插值的表達式之間的東西)。我們在`strings`數組中得到兩個值:`"Everything is "`和`"!"`。
之后為了我們示例的方便,我們使用`...`收集/剩余操作符(見本章早先的“擴散/剩余”部分)將所有后續的參數值收集到一個稱為`values`的數組中,雖說你本來當然可以把它們留作參數`strings`后面單獨的命名參數。
被收集進我們的`values`數組中的參數值,就是在字符串字面量中發現的,已經被求過值的插值表達式的結果。所以在我們的例子中`values`里唯一的元素顯然就是`awesome`。
你可以將這兩個數組考慮為:在`values`中的值原本是你拼接在`stings`的值之間的分隔符,而且如果你將所有的東西連接在一起,你就會得到完整的插值字符串值。
一個標簽型字符串字面量像是一個在插值表達式被求值之后,但是在最終的字符串被編譯之前的處理步驟,允許你在從字面量中產生字符串的過程中進行更多的控制。
一般來說,一個字符串字面量標簽函數(在前面的代碼段中是`foo(..)`)應當計算一個恰當的字符串值并返回它,所以你可以使用標簽型字符串字面量作為一個未打標簽的字符串字面量來使用:
```source-js
function tag(strings, ...values) {
return strings.reduce( function(s,v,idx){
return s + (idx > 0 ? values[idx-1] : "") + v;
}, "" );
}
var desc = "awesome";
var text = tag`Everything is ${desc}!`;
console.log( text ); // Everything is awesome!
```
在這個代碼段中,`tag(..)`是一個直通操作,因為它不實施任何特殊的修改,而只是使用`reduce(..)`來循環遍歷,并像一個未打標簽的字符串字面量一樣,將`strings`和`values`拼接/穿插在一起。
那么實際的用法是什么?有許多高級的用法超出了我們要在這里討論的范圍。但這里有一個格式化美元數字的簡單想法(有些像基本的本地化):
```source-js
function dollabillsyall(strings, ...values) {
return strings.reduce( function(s,v,idx){
if (idx > 0) {
if (typeof values[idx-1] == "number") {
// 看,也使用插值性字符串字面量!
s += `$${values[idx-1].toFixed( 2 )}`;
}
else {
s += values[idx-1];
}
}
return s + v;
}, "" );
}
var amt1 = 11.99,
amt2 = amt1 * 1.08,
name = "Kyle";
var text = dollabillsyall
`Thanks for your purchase, ${name}! Your
product cost was ${amt1}, which with tax
comes out to ${amt2}.`
console.log( text );
// Thanks for your purchase, Kyle! Your
// product cost was $11.99, which with tax
// comes out to $12.95.
```
如果在`values`數組中遇到一個`number`值,我們就在它前面放一個`"$"`并用`toFixed(2)`將它格式化為小數點后兩位有效。否則,我們就不碰這個值而讓它直通過去。
#### 原始字符串
在前一個代碼段中,我們的標簽函數接受的第一個參數值稱為`strings`,是一個數組。但是有一點兒額外的數據被包含了進來:所有字符串的原始未處理版本。你可以使用`.raw`屬性訪問這些原始字符串值,就像這樣:
```source-js
function showraw(strings, ...values) {
console.log( strings );
console.log( strings.raw );
}
showraw`Hello\nWorld`;
// [ "Hello
// World" ]
// [ "Hello\nWorld" ]
```
原始版本的值保留了原始的轉義序列`\n`(`\`和`n`是兩個分離的字符),但處理過的版本認為它是一個單獨的換行符。但是,早先提到的行終結符泛化操作,是對兩個值都實施的。
ES6帶來了一個內建函數,它可以用做字符串字面量的標簽:`String.raw(..)`。它簡單地直通`strings`值的原始版本:
```source-js
console.log( `Hello\nWorld` );
// Hello
// World
console.log( String.raw`Hello\nWorld` );
// Hello\nWorld
String.raw`Hello\nWorld`.length;
// 12
```
字符串字面量標簽的其他用法包括國際化,本地化,和許多其他的特殊處理。
## 箭頭函數
我們在本章早先接觸了函數中`this`綁定的復雜性,而且在本系列的?*this與對象原型*?中也以相當的篇幅講解過。理解普通函數中基于`this`的編程帶來的挫折是很重要的,因為這是ES6的新`=>`箭頭函數的主要動機。
作為與普通函數的比較,我們首先來展示一下箭頭函數看起來什么樣:
```source-js
function foo(x,y) {
return x + y;
}
// 對比
var foo = (x,y) => x + y;
```
箭頭函數的定義由一個參數列表(零個或多個參數,如果參數不是只有一個,需要有一個`( .. )`包圍這些參數)組成,緊跟著是一個`=>`符號,然后是一個函數體。
所以,在前面的代碼段中,箭頭函數只是`(x,y) => x + y`這一部分,而這個函數的引用剛好被賦值給了變量`foo`。
函數體僅在含有多于一個表達式,或者由一個非表達式語句組成時才需要用`{ .. }`括起來。如果僅含有一個表達式,而且你省略了外圍的`{ .. }`,那么在這個表達式前面就會有一個隱含的`return`,就像前面的代碼段中展示的那樣。
這里是一些其他種類的箭頭函數:
```source-js
var f1 = () => 12;
var f2 = x => x * 2;
var f3 = (x,y) => {
var z = x * 2 + y;
y++;
x *= 3;
return (x + y + z) / 2;
};
```
箭頭函數?*總是*?函數表達式;不存在箭頭函數聲明。而且很明顯它們都是匿名函數表達式 —— 它們沒有可以用于遞歸或者事件綁定/解除的命名引用 —— 但在第七章的“函數名”中將會講解為了調試的目的而存在的ES6函數名接口規則。
注意:?普通函數參數的所有功能對于箭頭函數都是可用的,包括默認值,解構,剩余參數,等等。
箭頭函數擁有漂亮,簡短的語法,這使得它們在表面上看起來對于編寫簡潔代碼很有吸引力。確實,幾乎所有關于ES6的文獻(除了這個系列中的書目)看起來都立即將箭頭函數僅僅認作“新函數”。
這說明在關于箭頭函數的討論中,幾乎所有的例子都是簡短的單語句工具,比如那些作為回調傳遞給各種工具的箭頭函數。例如:
```source-js
var a = [1,2,3,4,5];
a = a.map( v => v * 2 );
console.log( a ); // [2,4,6,8,10]
```
在這些情況下,你的內聯函數表達式很適合這種在一個單獨語句中快速計算并返回結果的模式,對于更繁冗的`function`關鍵字和語法來說箭頭函數確實看起來是一個很吸人,而且輕量的替代品。
大多數人看著這樣簡潔的例子都傾向于發出“哦……!啊……!”的感嘆,就像我想象中你剛剛做的那樣!
然而我要警示你的是,在我看來,使用箭頭函數的語法代替普通的,多語句函數,特別是那些可以被自然地表達為函數聲明的函數,是某種誤用。
回憶本章早前的字符串字面量標簽函數`dollabillsyall(..)`?—— 讓我們將它改為使用`=>`語法:
```source-js
var dollabillsyall = (strings, ...values) =>
strings.reduce( (s,v,idx) => {
if (idx > 0) {
if (typeof values[idx-1] == "number") {
// look, also using interpolated
// string literals!
s += `$${values[idx-1].toFixed( 2 )}`;
}
else {
s += values[idx-1];
}
}
return s + v;
}, "" );
```
在這個例子中,我做的唯一修改是刪除了`function`,`return`,和一些`{ .. }`,然后插入了`=>`和一個`var`。這是對代碼可讀性的重大改進嗎?呵呵。
實際上我會爭論,缺少`return`和外部的`{ .. }`在某種程度上模糊了這樣的事實:`reduce(..)`調用是函數`dollabillsyall(..)`中唯一的語句,而且它的結果是這個調用的預期結果。另外,那些受過訓練而習慣于在代碼中搜索`function`關鍵字來尋找作用域邊界的眼睛,現在需要搜索`=>`標志,在密集的代碼中這絕對會更加困難。
雖然不是一個硬性規則,但是我要說從`=>`箭頭函數轉換得來的可讀性,與被轉換的函數長度成反比。函數越長,`=>`能幫的忙越少;函數越短,`=>`的閃光之處就越多。
我覺得這樣做更明智也更合理:在你需要短的內聯函數表達式的地方采用`=>`,但保持你的一般長度的主函數原封不動。
### 不只是簡短的語法,而是`this`
曾經集中在`=>`上的大多數注意力都是它通過在你的代碼中除去`function`,`return`,和`{ .. }`來節省那些寶貴的擊鍵。
但是至此我們一直忽略了一個重要的細節。我在這一節最開始的時候說過,`=>`函數與`this`綁定行為密切相關。事實上,`=>`箭頭函數?*主要的設計目的*?就是以一種特定的方式改變`this`的行為,解決在`this`敏感的編碼中的一個痛點。
節省擊鍵是掩人耳目的東西,至多是一個誤導人的配角。
讓我們重溫本章早前的另一個例子:
```source-js
var controller = {
makeRequest: function(..){
var self = this;
btn.addEventListener( "click", function(){
// ..
self.makeRequest(..);
}, false );
}
};
```
我們使用了黑科技`var self = this`,然后引用了`self.makeRequest(..)`,因為在我們傳遞給`addEventListener(..)`的回調函數內部,`this`綁定將與`makeRequest(..)`本身中的`this`綁定不同。換句話說,因為`this`綁定是動態的,我們通過`self`變量退回到了可預測的詞法作用域。
在這其中我們終于可以看到`=>`箭頭函數主要的設計特性了。在箭頭函數內部,`this`綁定不是動態的,而是詞法的。在前一個代碼段中,如果我們在回調里使用一個箭頭函數,`this`將會不出所料地成為我們希望它成為的東西。
考慮如下代碼:
```source-js
var controller = {
makeRequest: function(..){
btn.addEventListener( "click", () => {
// ..
this.makeRequest(..);
}, false );
}
};
```
前面代碼段的箭頭函數中的詞法`this`現在指向的值與外圍的`makeRequest(..)`函數相同。換句話說,`=>`是`var self = this`的語法上的替代品。
在`var self = this`(或者,另一種選擇是,`.bind(this)`調用)通常可以幫忙的情況下,`=>`箭頭函數是一個基于相同原則的很好的替代操作。聽起來很棒,是吧?
沒那么簡單。
如果`=>`取代`var self = this`或`.bind(this)`可以工作,那么猜猜`=>`用于一個?*不需要*?`var self = this`就能工作的`this`敏感的函數會發生么?你可能會猜到它將會把事情搞砸。沒錯。
考慮如下代碼:
```source-js
var controller = {
makeRequest: (..) => {
// ..
this.helper(..);
},
helper: (..) => {
// ..
}
};
controller.makeRequest(..);
```
雖然我們以`controller.makeRequest(..)`的方式進行了調用,但是`this.helper`引用失敗了,因為這里的`this`沒有像平常那樣指向`controller`。那么它指向哪里?它通過詞法繼承了外圍的作用域中的`this`。在前面的代碼段中,它是全局作用域,`this`指向了全局作用域。呃。
除了詞法的`this`以外,箭頭函數還擁有詞法的`arguments`?—— 它們沒有自己的`arguments`數組,而是從它們的上層繼承下來 —— 同樣還有詞法的`super`和`new.target`(參見第三章的“類”)。
所以,關于`=>`在什么情況下合適或不合適,我們現在可以推論出一組更加微妙的規則:
* 如果你有一個簡短的,單語句內聯函數表達式,它唯一的語句是某個計算后的值的`return`語句,*并且*?這個函數沒有在它內部制造一個`this`引用,*并且*?沒有自引用(遞歸,事件綁定/解除),*并且*?你合理地預期這個函數絕不會變得需要`this`引用或自引用,那么你就可能安全地將它重構為一個`=>`箭頭函數。
* 如果你有一個內部函數表達式,它依賴于外圍函數的`var self = this`黑科技或者`.bind(this)`調用來確保正確的`this`綁定,那么這個內部函數表達式就可能安全地變為一個`=>`箭頭函數。
* 如果你有一個內部函數表達式,它依賴于外圍函數的類似于`var args = Array.prototype.slice.call(arguments)`這樣的東西來制造一個`arguments`的詞法拷貝,那么這個內部函數就可能安全地變為一個`=>`箭頭函數。
* 對于其他的所有東西 —— 普通函數聲明,較長的多語句函數表達式,需要詞法名稱標識符進行自引用(遞歸等)的函數,和任何其他不符合前述性質的函數 —— 你就可能應當避免`=>`函數語法。
底線:`=>`與`this`,`arguments`,和`super`的詞法綁定有關。它們是ES6為了修正一些常見的問題而被有意設計的特性,而不是為了修正bug,怪異的代碼,或者錯誤。
不要相信任何說`=>`主要是,或者幾乎是,為了減少幾下擊鍵的炒作。無論你是省下還是浪費了這幾下擊鍵,你都應當確切地知道你打入的每個字母是為了做什么。
提示:?如果你有一個函數,由于上述各種清楚的原因而不適合成為一個`=>`箭頭函數,但同時它又被聲明為一個對象字面量的一部分,那么回想一下本章早先的“簡約方法”,它有簡短函數語法的另一種選擇。
對于如何/為何選用一個箭頭函數,如果你喜歡一個可視化的決策圖的話:
[](https://github.com/getify/You-Dont-Know-JS/blob/1ed-zh-CN/es6%20%26%20beyond/fig1.png)
## `for..of`循環
伴隨著我們熟知的JavaScript`for`和`for..in`循環,ES6增加了一個`for..of`循環,它循環遍歷一組由一個?*迭代器(iterator)*產生的值。
你使用`for..of`循環遍歷的值必須是一個?*可迭代對象(iterable)*,或者它必須是一個可以被強制轉換/封箱(參見本系列的?*類型與文法*)為一個可迭代對象的值。一個可迭代對象只不過是一個可以生成迭代器的對象,然后由循環使用這個迭代器。
讓我們比較`for..of`與`for..in`來展示它們的區別:
```source-js
var a = ["a","b","c","d","e"];
for (var idx in a) {
console.log( idx );
}
// 0 1 2 3 4
for (var val of a) {
console.log( val );
}
// "a" "b" "c" "d" "e"
```
如你所見,`for..in`循環遍歷數組`a`中的鍵/索引,而`for.of`循環遍歷`a`中的值。
這是前面代碼段中`for..of`的前ES6版本:
```source-js
var a = ["a","b","c","d","e"],
k = Object.keys( a );
for (var val, i = 0; i < k.length; i++) {
val = a[ k[i] ];
console.log( val );
}
// "a" "b" "c" "d" "e"
```
而這是一個ES6版本的非`for..of`等價物,它同時展示了手動迭代一個迭代器(見第三章的“迭代器”):
```source-js
var a = ["a","b","c","d","e"];
for (var val, ret, it = a[Symbol.iterator]();
(ret = it.next()) && !ret.done;
) {
val = ret.value;
console.log( val );
}
// "a" "b" "c" "d" "e"
```
在幕后,`for..of`循環向可迭代對象要來一個迭代器(使用內建的`Symbol.iterator`;參見第七章的“通用Symbols”),然后反復調用這個迭代器并將它產生的值賦值給循環迭代的變量。
在JavaScript標準的內建值中,默認為可迭代對象的(或提供可迭代能力的)有:
* 數組
* 字符串
* Generators(見第三章)
* 集合/類型化數組(見第五章)
警告:?普通對象默認是不適用于`for..of`循環的。因為他們沒有默認的迭代器,這是有意為之的,不是一個錯誤。但是,我們不會進一步探究這其中微妙的原因。在第三章的“迭代器”中,我們將看到如何為我們自己的對象定義迭代器,這允許`for..of`遍歷任何對象來得到我們定義的一組值。
這是如何遍歷一個基本類型的字符串中的字符:
```source-js
for (var c of "hello") {
console.log( c );
}
// "h" "e" "l" "l" "o"
```
基本類型字符串`"hello"`被強制轉換/封箱為等價的`String`對象包裝器,它是默認就是一個可迭代對象。
在`for (XYZ of ABC)..`中,`XYZ`子句既可以是一個賦值表達式也可以是一個聲明,這與`for`和`for..in`中相同的子句一模一樣。所以你可以做這樣的事情:
```source-js
var o = {};
for (o.a of [1,2,3]) {
console.log( o.a );
}
// 1 2 3
for ({x: o.a} of [ {x: 1}, {x: 2}, {x: 3} ]) {
console.log( o.a );
}
// 1 2 3
```
與其他的循環一樣,使用`break`,`continue`,`return`(如果是在一個函數中),以及拋出異常,`for..of`循環可以被提前終止。在任何這些情況下,迭代器的`return(..)`函數(如果存在的話)都會被自動調用,以便讓迭代器進行必要的清理工作。
注意:?可迭代對象與迭代器的完整內容參見第三章的“迭代器”。
## 正則表達式擴展
讓我們承認吧:長久以來在JS中正則表達式都沒怎么改變過。所以一件很棒的事情是,在ES6中它們終于學會了一些新招數。我們將在這里簡要地講解一下新增的功能,但是正則表達式整體的話題是如此厚重,以至于如果你需要復習一下的話你需要找一些關于它的專門章節/書籍(有許多!)。
### Unicode標志
我們將在本章稍后的“Unicode”一節中講解關于Unicode的更多細節。在此,我們將僅僅簡要地看一下ES6+正則表達式的新`u`標志,它使這個正則表達式的Unicode匹配成為可能。
JavaScript字符串通常被解釋為16位字符的序列,它們對應于?*基本多文種平面(Basic Multilingual Plane (BMP))* http://en.wikipedia.org/wiki/Plane_%28Unicode%29中的字符。但是有許多UTF-16字符在這個范圍以外,而且字符串可能含有這些多字節字符。
在ES6之前,正則表達式只能基于BMP字符進行匹配,這意味著在匹配時那些擴展字符被看作是兩個分離的字符。這通常不理想。
所以,在ES6中,`u`標志告訴正則表達式使用Unicode(UTF-16)字符的解釋方式來處理字符串,這樣一來一個擴展的字符將作為一個單獨的實體被匹配。
警告:?盡管名字的暗示是這樣,但是“UTF-16”并不嚴格地意味著16位。現代的Unicode使用21位,而且像UTF-8和UTF-16這樣的標準大體上是指有多少位用于表示一個字符。
一個例子(直接從ES6語言規范中拿來的): ?? (G大調音樂符號)是Unicode代碼點U+1D11E(0x1D11E)。
如果這個字符出現在一個正則表達式范例中(比如`/??/`),標準的BMP解釋方式將認為它是需要被匹配的兩個字符(0xD834和0xDD1E)。但是ES6新的Unicode敏感模式意味著`/??/u`(或者Unicode的轉義形式`/\u{1D11E}/u`)將會把`"??"`作為一個單獨的字符在一個字符串中進行匹配。
你可能想知道為什么這很重要。在非Unicode的BMP模式下,這個正則表達式范例被看作兩個分離的字符,但它仍然可以在一個含有`"??"`字符的字符串中找到匹配,如果你試一下就會看到:
```source-js
/??/.test( "??-clef" ); // true
```
重要的是匹配的長度。例如:
```source-js
/^.-clef/ .test( "??-clef" ); // false
/^.-clef/u.test( "??-clef" ); // true
```
這個范例中的`^.-clef`說要在普通的`"-clef"`文本前面只匹配一個單獨的字符。在標準的BMP模式下,這個匹配會失敗(因為是兩個字符),但是在Unicode模式標志位`u`打開的情況下,這個匹配會成功(一個字符)。
另外一個重要的注意點是,`u`使像`+`和`*`這樣的量詞實施于作為一個單獨字符的整個Unicode代碼點,而不僅僅是字符的?*低端替代符*(也就是符號最右邊的一半)。對于出現在字符類中的Unicode字符也是一樣,比如`/[??-??]/u`。
注意:?還有許多關于`u`在正則表達式中行為的細節,對此Mathias Bynens([https://twitter.com/mathias)撰寫了大量的作品(https://mathiasbynens.be/notes/es6-unicode-regex)。](https://twitter.com/mathias)(https://mathiasbynens.be/notes/es6-unicode-regex)
### 粘性標志
另一個加入ES6正則表達式的模式標志是`y`,它經常被稱為“粘性模式(sticky mode)”。*粘性*?實質上意味著正則表達式在它開始時有一個虛擬的錨點,這個錨點使正則表達式僅以自己的`lastIndex`屬性所指示的位置為起點進行匹配。
為了展示一下,讓我們考慮兩個正則表達式,第一個沒有使用粘性模式而第二個有:
```source-js
var re1 = /foo/,
str = "++foo++";
re1.lastIndex; // 0
re1.test( str ); // true
re1.lastIndex; // 0 —— 沒有更新
re1.lastIndex = 4;
re1.test( str ); // true —— `lastIndex`被忽略了
re1.lastIndex; // 4 —— 沒有更新
```
關于這個代碼段可以觀察到三件事:
* `test(..)`根本不在意`lastIndex`的值,而總是從輸入字符串的開始實施它的匹配。
* 因為我們的模式沒有輸入的起始錨點`^`,所以對`"foo"`的搜索可以在整個字符串上自由向前移動。
* `lastIndex`沒有被`test(..)`更新。
現在,讓我們試一下粘性模式的正則表達式:
```source-js
var re2 = /foo/y, // <-- 注意粘性標志`y`
str = "++foo++";
re2.lastIndex; // 0
re2.test( str ); // false —— 在`0`沒有找到“foo”
re2.lastIndex; // 0
re2.lastIndex = 2;
re2.test( str ); // true
re2.lastIndex; // 5 —— 在前一次匹配后更新了
re2.test( str ); // false
re2.lastIndex; // 0 —— 在前一次匹配失敗后重置
```
于是關于粘性模式我們可以觀察到一些新的事實:
* `test(..)`在`str`中使用`lastIndex`作為唯一精確的位置來進行匹配。在尋找匹配時不會發生向前的移動 —— 匹配要么出現在`lastIndex`的位置,要么就不存在。
* 如果發生了一個匹配,`test(..)`就更新`lastIndex`使它指向緊隨匹配之后的那個字符。如果匹配失敗,`test(..)`就將`lastIndex`重置為`0`。
沒有使用`^`固定在輸入起點的普通非粘性范例可以自由地在字符串中向前移動來搜索匹配。但是粘性模式制約這個范例僅在`lastIndex`的位置進行匹配。
正如我在這一節開始時提到過的,另一種考慮的方式是,`y`暗示著一個虛擬的錨點,它位于正好相對于(也就是制約著匹配的起始位置)`lastIndex`位置的范例的開頭。
警告:?在關于這個話題的以前的文獻中,這種行為曾經被聲稱為`y`像是在范例中暗示著一個`^`(輸入的起始)錨點。這是不準確的。我們將在稍后的“錨定粘性”中講解更多細節。
#### 粘性定位
對反復匹配使用`y`可能看起來是一種奇怪的限制,因為匹配沒有向前移動的能力,你不得不手動保證`lastIndex`恰好位于正確的位置上。
這是一種可能的場景:如果你知道你關心的匹配總是會出現在一個數字(例如,`0`,`10`,`20`,等等)倍數的位置。那么你就可以只構建一個受限的范例來匹配你關心的東西,然后在每次匹配那些固定位置之前手動設置`lastIndex`。
考慮如下代碼:
```source-js
var re = /f../y,
str = "foo far fad";
str.match( re ); // ["foo"]
re.lastIndex = 10;
str.match( re ); // ["far"]
re.lastIndex = 20;
str.match( re ); // ["fad"]
```
然而,如果你正在解析一個沒有像這樣被格式化為固定位置的字符串,在每次匹配之前搞清楚為`lastIndex`設置什么東西的做法可能會難以維系。
這里有一個微妙之處要考慮。`y`要求`lastIndex`位于發生匹配的準確位置。但它不嚴格要求?*你*?來手動設置`lastIndex`。
取而代之的是,你可以用這樣的方式構建你的正則表達式:它們在每次主匹配中都捕獲你所關心的東西的前后所有內容,直到你想要進行下一次匹配的東西為止。
因為`lastIndex`將被設置為一個匹配末尾之后的下一個字符,所以如果你已經匹配了到那個位置的所有東西,`lastIndex`將總是位于下次`y`范例開始的正確位置。
警告:?如果你不能像這樣足夠范例化地預知輸入字符串的結構,這種技術可能不合適,而且你可能不應使用`y`。
擁有結構化的字符串輸入,可能是`y`能夠在一個字符串上由始至終地進行反復匹配的最實際場景。考慮如下代碼:
```source-js
var re = /\d+\.\s(.*?)(?:\s|$)/y
str = "1\. foo 2\. bar 3\. baz";
str.match( re ); // [ "1\. foo ", "foo" ]
re.lastIndex; // 7 —— 正確位置!
str.match( re ); // [ "2\. bar ", "bar" ]
re.lastIndex; // 14 —— 正確位置!
str.match( re ); // ["3\. baz", "baz"]
```
這能夠工作是因為我事先知道輸入字符串的結構:總是有一個像`"1\. "`這樣的數字的前綴出現在期望的匹配(`"foo"`,等等)之前,而且它后面要么是一個空格,要么就是字符串的末尾(`$`錨點)。所以我構建的正則表達式在每次主匹配中捕獲了所有這一切,然后我使用一個匹配分組`( )`使我真正關心的東西被方便地分離出來。
在第一次匹配(`"1\. foo "`)之后,`lastIndex`是`7`,它已經是開始下一次匹配`"2\. bar "`所需的位置了,如此類推。
如果你要使用粘性模式`y`進行反復匹配,那么你就可能想要像我們剛剛展示的那樣尋找一個機會自動地定位`lastIndex`。
#### 粘性對比全局
一些讀者可能意識到,你可以使用全局匹配標志位`g`和`exec(..)`方法來模擬某些像`lastIndex`相對匹配的東西,就像這樣:
```source-js
var re = /o+./g, // <-- 看,`g`!
str = "foot book more";
re.exec( str ); // ["oot"]
re.lastIndex; // 4
re.exec( str ); // ["ook"]
re.lastIndex; // 9
re.exec( str ); // ["or"]
re.lastIndex; // 13
re.exec( str ); // null —— 沒有更多的匹配了!
re.lastIndex; // 0 —— 現在重新開始!
```
雖然使用`exec(..)`的`g`范例確實從`lastIndex`的當前值開始它們的匹配,而且也在每次匹配(或失敗)之后更新`lastIndex`,但這與`y`的行為不是相同的東西。
注意前面代碼段中被第二個`exec(..)`調用匹配并找到的`"ook"`,被定位在位置`6`,即便在這個時候`lastIndex`是`4`(前一次匹配的末尾)。為什么?因為正如我們前面講過的,非粘性匹配可以在它們的匹配過程中自由地向前移動。一個粘性模式表達式在這里將會失敗,因為它不允許向前移動。
除了也許不被期望的向前移動的匹配行為以外,使用`g`代替`y`的另一個缺點是,`g`改變了一些匹配方法的行為,比如`str.match(re)`。
考慮如下代碼:
```source-js
var re = /o+./g, // <-- 看,`g`!
str = "foot book more";
str.match( re ); // ["oot","ook","or"]
```
看到所有的匹配是如何一次性地被返回的嗎?有時這沒問題,但有時這不是你想要的。
與`test(..)`和`match(..)`這樣的工具一起使用,粘性標志位`y`將給你一次一個的推進式的匹配。只要保證每次匹配時`lastIndex`總是在正確的位置上就行!
#### 錨定粘性
正如我們早先被警告過的,將粘性模式認為是暗含著一個以`^`開頭的范例是不準確的。在正則表達式中錨點`^`擁有獨特的含義,它?*沒有*?被粘性模式改變。`^`*總是*?一個指向輸入起點的錨點,而且?*不*?以任何方式相對于`lastIndex`。
在這個問題上,除了糟糕/不準確的文檔,一個在Firefox中進行的老舊的前ES6粘性模式實驗不幸地加深了這種困惑,它確實?*曾經*?使`^`相對于`lastIndex`,所以這種行為曾經存在了許多年。
ES6選擇不這么做。`^`在一個范例中絕對且唯一地意味著輸入的起點。
這樣的后果是,一個像`/^foo/y`這樣的范例將總是僅在一個字符串的開頭找到`"foo"`匹配,*如果它被允許在那里匹配的話*。如果`lastIndex`不是`0`,匹配就會失敗。考慮如下代碼:
```source-js
var re = /^foo/y,
str = "foo";
re.test( str ); // true
re.test( str ); // false
re.lastIndex; // 0 —— 失敗之后被重置
re.lastIndex = 1;
re.test( str ); // false —— 由于定位而失敗
re.lastIndex; // 0 —— 失敗之后被重置
```
底線:`y`加`^`加`lastIndex > 0`是一種不兼容的組合,它將總是導致失敗的匹配。
注意:?雖然`y`不會以任何方式改變`^`的含義,但是多行模式`m`*會*,這樣`^`就意味著輸入的起點?*或者*?一個換行之后的文本的起點。所以,如果你在一個范例中組合使用`y`和`m`,你會在一個字符串中發現多個開始于`^`的匹配。但是要記住:因為它的粘性`y`,將不得不在后續的每次匹配時確保`lastIndex`被置于正確的換行的位置(可能是通過匹配到行的末尾),否者后續的匹配將不會執行。
### 正則表達式`flags`
在ES6之前,如果你想要檢查一個正則表達式來看看它被施用了什么標志位,你需要將它們 —— 諷刺的是,可能是使用另一個正則表達式 —— 從`source`屬性的內容中解析出來,就像這樣:
```source-js
var re = /foo/ig;
re.toString(); // "/foo/ig"
var flags = re.toString().match( /\/([gim]*)$/ )[1];
flags; // "ig"
```
在ES6中,你現在可以直接得到這些值,使用新的`flags`屬性:
```source-js
var re = /foo/ig;
re.flags; // "gi"
```
雖然是個細小的地方,但是ES6規范要求表達式的標志位以`"gimuy"`的順序羅列,無論原本的范例中是以什么順序指定的。這就是出現`/ig`和`"gi"`的區別的原因。
是的,標志位被指定和羅列的順序無所謂。
ES6的另一個調整是,如果你向構造器`RegExp(..)`傳遞一個既存的正則表達式,它現在是`flags`敏感的:
```source-js
var re1 = /foo*/y;
re1.source; // "foo*"
re1.flags; // "y"
var re2 = new RegExp( re1 );
re2.source; // "foo*"
re2.flags; // "y"
var re3 = new RegExp( re1, "ig" );
re3.source; // "foo*"
re3.flags; // "gi"
```
在ES6之前,構造`re3`將拋出一個錯誤,但是在ES6中你可以在復制時覆蓋標志位。
## 數字字面量擴展
在ES5之前,數字字面量看起來就像下面的東西 —— 八進制形式沒有被官方指定,唯一被允許的是各種瀏覽器已經實質上達成一致的一種擴展:
```source-js
var dec = 42,
oct = 052,
hex = 0x2a;
```
注意:?雖然你用不同的進制來指定一個數字,但是數字的數學值才是被存儲的東西,而且默認的輸出解釋方式總是10進制的。前面代碼段中的三個變量都在它們當中存儲了值`42`。
為了進一步說明`052`是一種非標準形式擴展,考慮如下代碼:
```source-js
Number( "42" ); // 42
Number( "052" ); // 52
Number( "0x2a" ); // 42
```
ES5繼續允許這種瀏覽器擴展的八進制形式(包括這樣的不一致性),除了在strict模式下,八進制字面量(`052`)是不允許的。做出這種限制的主要原因是,許多開發者似乎習慣于下意識地為了將代碼對齊而在十進制的數字前面前綴`0`,然后遭遇他們完全改變了數字的值的意外!
ES6延續了除十進制數字之外的數字字面量可以被表示的遺留的改變/種類。現在有了一種官方的八進制形式,一種改進了的十六進制形式,和一種全新的二進制形式。由于Web兼容性的原因,在非strict模式下老式的八進制形式`052`將繼續是合法的,但其實應當永遠不再被使用了。
這些是新的ES6數字字面形式:
```source-js
var dec = 42,
oct = 0o52, // or `0O52` :(
hex = 0x2a, // or `0X2a` :/
bin = 0b101010; // or `0B101010` :/
```
唯一允許的小數形式是十進制的。八進制,十六進制,和二進制都是整數形式。
而且所有這些形式的字符串表達形式都是可以被強制轉換/變換為它們的數字等價物的:
```source-js
Number( "42" ); // 42
Number( "0o52" ); // 42
Number( "0x2a" ); // 42
Number( "0b101010" ); // 42
```
雖然嚴格來說不是ES6新增的,但一個鮮為人知的事實是你其實可以做反方向的轉換(好吧,某種意義上的):
```source-js
var a = 42;
a.toString(); // "42" —— 也可使用`a.toString( 10 )`
a.toString( 8 ); // "52"
a.toString( 16 ); // "2a"
a.toString( 2 ); // "101010"
```
事實上,以這種方你可以用從`2`到`36`的任何進制表達一個數字,雖然你會使用標準進制 —— 2,8,10,和16 ——之外的情況非常少見。
## Unicode
我只能說這一節不是一個窮盡了“關于Unicode你想知道的一切”的資料。我想講解的是,你需要知道在ES6中對Unicode改變了什么,但是我們不會比這深入太多。Mathias Bynens ([http://twitter.com/mathias](http://twitter.com/mathias)) 大量且出色地撰寫/講解了關于JS和Unicode (參見?[https://mathiasbynens.be/notes/javascript-unicode](https://mathiasbynens.be/notes/javascript-unicode)?和?[http://fluentconf.com/javascript-html-2015/public/content/2015/02/18-javascript-loves-unicode)。](http://fluentconf.com/javascript-html-2015/public/content/2015/02/18-javascript-loves-unicode)
從`0x0000`到`0xFFFF`范圍內的Unicode字符包含了所有的標準印刷字符(以各種語言),它們都是你可能看到過和互動過的。這組字符被稱為?*基本多文種平面(Basic Multilingual Plane (BMP))*。BMP甚至包含像這個酷雪人一樣的有趣字符: ? (U+2603)。
在這個BMP集合之外還有許多擴展的Unicode字符,它們的范圍一直到`0x10FFFF`。這些符號經常被稱為?*星形(astral)*?符號,這正是BMP之外的字符的16組?*平面*?(也就是,分層/分組)的名稱。星形符號的例子包括?? (U+1D11E)和??(U+1F4A9)。
在ES6之前,JavaScript字符串可以使用Unicode轉義來指定Unicode字符,例如:
```source-js
var snowman = "\u2603";
console.log( snowman ); // "?"
```
然而,`\uXXXX`Unicode轉義僅支持四個十六進制字符,所以用這種方式表示你只能表示BMP集合中的字符。要在ES6以前使用Unicode轉義表示一個星形字符,你需要使用一個?*代理對(surrogate pair)*?—— 基本上是兩個經特殊計算的Unicode轉義字符放在一起,被JS解釋為一個單獨星形字符:
```source-js
var gclef = "\uD834\uDD1E";
console.log( gclef ); // "??"
```
在ES6中,我們現在有了一種Unicode轉義的新形式(在字符串和正則表達式中),稱為Unicode?*代碼點轉義*:
```source-js
var gclef = "\u{1D11E}";
console.log( gclef ); // "??"
```
如你所見,它的區別是出現在轉義序列中的`{ }`,它允許轉義序列中包含任意數量的十六進制字符。因為你只需要六個就可以表示在Unicode中可能的最高代碼點(也就是,0x10FFFF),所以這是足夠的。
### Unicode敏感的字符串操作
在默認情況下,JavaScript字符串操作和方法對字符串值中的星形符號是不敏感的。所以,它們獨立地處理每個BMP字符,即便是可以組成一個單獨字符的兩半代理。考慮如下代碼:
```source-js
var snowman = "?";
snowman.length; // 1
var gclef = "??";
gclef.length; // 2
```
那么,我們如何才能正確地計算這樣的字符串的長度呢?在這種場景下,下面的技巧可以工作:
```source-js
var gclef = "??";
[...gclef].length; // 1
Array.from( gclef ).length; // 1
```
回想一下本章早先的“`for..of`循環”一節,ES6字符串擁有內建的迭代器。這個迭代器恰好是Unicode敏感的,這意味著它將自動地把一個星形符號作為一個單獨的值輸出。我們在一個數組字面量上使用擴散操作符`...`,利用它創建了一個字符串符號的數組。然后我們只需檢查這個結果數組的長度。ES6的`Array.from(..)`基本上與`[...XYZ]`做的事情相同,不過我們將在第六章中講解這個工具的細節。
警告:?應當注意的是,相對地講,與理論上經過優化的原生工具/屬性將做的事情比起來,僅僅為了得到一個字符串的長度就構建并耗盡一個迭代器在性能上的代價是高昂的。
不幸的是,完整的答案并不簡單或直接。除了代理對(字符串迭代器可以搞定的),一些特殊的Unicode代碼點有其他特殊的行為,解釋起來非常困難。例如,有一組代碼點可以修改前一個相鄰的字符,稱為?*組合變音符號(Combining Diacritical Marks)*
考慮這兩個數組的輸出:
```source-js
console.log( s1 ); // "é"
console.log( s2 ); // "é"
```
它們看起來一樣,但它們不是!這是我們如何創建`s1`和`s2`的:
```source-js
var s1 = "\xE9",
s2 = "e\u0301";
```
你可能猜到了,我們前面的`length`技巧對`s2`不管用:
```source-js
[...s1].length; // 1
[...s2].length; // 2
```
那么我們能做什么?在這種情況下,我們可以使用ES6的`String#normalize(..)`工具,在查詢這個值的長度前對它實施一個?*Unicode正規化操作*:
```source-js
var s1 = "\xE9",
s2 = "e\u0301";
s1.normalize().length; // 1
s2.normalize().length; // 1
s1 === s2; // false
s1 === s2.normalize(); // true
```
實質上,`normalize(..)`接受一個`"e\u0301"`這樣的序列,并把它正規化為`\xE9`。正規化甚至可以組合多個相鄰的組合符號,如果存在適合他們組合的Unicode字符的話:
```source-js
var s1 = "o\u0302\u0300",
s2 = s1.normalize(),
s3 = "?";
s1.length; // 3
s2.length; // 1
s3.length; // 1
s2 === s3; // true
```
不幸的是,這里的正規化也不完美。如果你有多個組合符號在修改一個字符,你可能不會得到你所期望的長度計數,因為一個被獨立定義的,可以表示所有這些符號組合的正規化字符可能不存在。例如:
```source-js
var s1 = "e\u0301\u0330";
console.log( s1 ); // "e??"
s1.normalize().length; // 2
```
你越深入這個兔子洞,你就越能理解要得到一個“長度”的精確定義是很困難的。我們在視覺上看到的作為一個單獨字符繪制的東西 —— 更精確地說,它稱為一個?*字形*?—— 在程序處理的意義上不總是嚴格地關聯到一個單獨的“字符”上。
提示:?如果你就是想看看這個兔子洞有多深,看看“字形群集邊界(Grapheme Cluster Boundaries)”算法([http://www.Unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries)。](http://www.unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries)
### 字符定位
與長度的復雜性相似,“在位置2上的字符是什么?”,這么問的意思究竟是什么?前ES6的原生答案來自`charAt(..)`,它不會遵守一個星形字符的原子性,也不會考慮組合符號。
考慮如下代碼:
```source-js
var s1 = "abc\u0301d",
s2 = "ab\u0107d",
s3 = "ab\u{1d49e}d";
console.log( s1 ); // "ab?d"
console.log( s2 ); // "ab?d"
console.log( s3 ); // "ab??d"
s1.charAt( 2 ); // "c"
s2.charAt( 2 ); // "?"
s3.charAt( 2 ); // "" <-- 不可打印的代理字符
s3.charAt( 3 ); // "" <-- 不可打印的代理字符
```
那么,ES6會給我們Unicode敏感版本的`charAt(..)`嗎?不幸的是,不。在本書寫作時,在后ES6的考慮之中有一個這樣的工具的提案。
但是使用我們在前一節探索的東西(當然也帶著它的限制!),我們可以黑一個ES6的答案:
```source-js
var s1 = "abc\u0301d",
s2 = "ab\u0107d",
s3 = "ab\u{1d49e}d";
[...s1.normalize()][2]; // "?"
[...s2.normalize()][2]; // "?"
[...s3.normalize()][2]; // "??"
```
警告:?提醒一個早先的警告:在每次你想得到一個單獨的字符時構建并耗盡一個迭代器……在性能上不是很理想。對此,希望我們很快能在后ES6時代得到一個內建的,優化過的工具。
那么`charCodeAt(..)`工具的Unicode敏感版本呢?ES6給了我們`codePointAt(..)`:
```source-js
var s1 = "abc\u0301d",
s2 = "ab\u0107d",
s3 = "ab\u{1d49e}d";
s1.normalize().codePointAt( 2 ).toString( 16 );
// "107"
s2.normalize().codePointAt( 2 ).toString( 16 );
// "107"
s3.normalize().codePointAt( 2 ).toString( 16 );
// "1d49e"
```
那么從另一個方向呢?`String.fromCharCode(..)`的Unicode敏感版本是ES6的`String.fromCodePoint(..)`:
```source-js
String.fromCodePoint( 0x107 ); // "?"
String.fromCodePoint( 0x1d49e ); // "??"
```
那么等一下,我們能組合`String.fromCodePoint(..)`與`codePointAt(..)`來得到一個剛才的Unicode敏感`charAt(..)`的更好版本嗎?是的!
```source-js
var s1 = "abc\u0301d",
s2 = "ab\u0107d",
s3 = "ab\u{1d49e}d";
String.fromCodePoint( s1.normalize().codePointAt( 2 ) );
// "?"
String.fromCodePoint( s2.normalize().codePointAt( 2 ) );
// "?"
String.fromCodePoint( s3.normalize().codePointAt( 2 ) );
// "??"
```
還有好幾個字符串方法我們沒有在這里講解,包括`toUpperCase()`,`toLowerCase()`,`substring(..)`,`indexOf(..)`,`slice(..)`,以及其他十幾個。它們中沒有任何一個為了完全支持Unicode而被改變或增強過,所以在處理含有星形符號的字符串是,你應當非常小心 —— 可能干脆回避它們!
還有幾個字符串方法為了它們的行為而使用正則表達式,比如`replace(..)`和`match(..)`。值得慶幸的是,ES6為正則表達式帶來了Unicode支持,正如我們在本章早前的“Unicode標志”中講解過的那樣。
好了,就是這些!有了我們剛剛講過的各種附加功能,JavaScript的Unicode字符串支持要比前ES6時代好太多了(雖然還不完美)。
### Unicode標識符名稱
Unicode還可以被用于標識符名稱(變量,屬性,等等)。在ES6之前,你可以通過Unicode轉義這么做,比如:
```source-js
var \u03A9 = 42;
// 等同于:var Ω = 42;
```
在ES6中,你還可以使用前面講過的代碼點轉義語法:
```source-js
var \u{2B400} = 42;
// 等同于:var ?? = 42;
```
關于究竟哪些Unicode字符被允許使用,有一組復雜的規則。另外,有些字符只要不是標識符名稱的第一個字符就允許使用。
注意:?關于所有這些細節,Mathias Bynens寫了一篇了不起的文章 ([https://mathiasbynens.be/notes/javascript-identifiers-es6)。](https://mathiasbynens.be/notes/javascript-identifiers-es6)
很少有理由,或者是為了學術上的目的,才會在標識符名稱中使用這樣不尋常的字符。你通常不會因為依靠這些深奧的功能編寫代碼而感到舒服。
## Symbol
在ES6中,長久以來首次,有一個新的基本類型被加入到了JavaScript:`symbol`。但是,與其他的基本類型不同,symbol沒有字面形式。
這是你如何創建一個symbol:
```source-js
var sym = Symbol( "some optional description" );
typeof sym; // "symbol"
```
一些要注意的事情是:
* 你不能也不應該將`new`與`Symbol(..)`一起使用。它不是一個構造器,你也不是在產生一個對象。
* 被傳入`Symbol(..)`的參數是可選的。如果傳入的話,它應當是一個字符串,為symbol的目的給出一個友好的描述。
* `typeof`的輸出是一個新的值(`"symbol"`),這是識別一個symbol的主要方法。
如果描述被提供的話,它僅僅用于symbol的字符串化表示:
```source-js
sym.toString(); // "Symbol(some optional description)"
```
與基本字符串值如何不是`String`的實例的原理很相似,symbol也不是`Symbol`的實例。如果,由于某些原因,你想要為一個symbol值構建一個封箱的包裝器對像,你可以做如下的事情:
```source-js
sym instanceof Symbol; // false
var symObj = Object( sym );
symObj instanceof Symbol; // true
symObj.valueOf() === sym; // true
```
注意:?在這個代碼段中的`symObj`和`sym`是可以互換使用的;兩種形式可以在symbol被用到的地方使用。沒有太多的理由要使用封箱的包裝對象形式(`symObj`),而不用基本類型形式(`sym`)。和其他基本類型的建議相似,使用`sym`而非`symObj`可能是最好的。
一個symbol本身的內部值 —— 稱為它的`name`?—— 被隱藏在代碼之外而不能取得。你可以認為這個symbol的值是一個自動生成的,(在你的應用程序中)獨一無二的字符串值。
但如果這個值是隱藏且不可取得的,那么擁有一個symbol還有什么意義?
一個symbol的主要意義是創建一個不會和其他任何值沖突的類字符串值。所以,舉例來說,可以考慮將一個symbol用做表示一個事件的名稱的值:
```source-js
const EVT_LOGIN = Symbol( "event.login" );
```
然后你可以在一個使用像`"event.login"`這樣的一般字符串字面量的地方使用`EVT_LOGIN`:
```source-js
evthub.listen( EVT_LOGIN, function(data){
// ..
} );
```
其中的好處是,`EVT_LOGIN`持有一個不能被其他任何值所(有意或無意地)重復的值,所以在哪個事件被分發或處理的問題上不可能存在任何含糊。
注意:?在前面的代碼段的幕后,幾乎可以肯定地認為`evthub`工具使用了`EVT_LOGIN`參數值的symbol值作為某個跟蹤事件處理器的內部對象的屬性/鍵。如果`evthub`需要將symbol值作為一個真實的字符串使用,那么它將需要使用`String(..)`或者`toString(..)`進行明確強制轉換,因為symbol的隱含字符串強制轉換是不允許的。
你可能會將一個symbol直接用做一個對象中的屬性名/鍵,如此作為一個你想將之用于隱藏或元屬性的特殊屬性。重要的是,要知道雖然你試圖這樣對待它,但是它?*實際上*?并不是隱藏或不可接觸的屬性。
考慮這個實現了?*單例*?模式行為的模塊 —— 也就是,它僅允許自己被創建一次:
```source-js
const INSTANCE = Symbol( "instance" );
function HappyFace() {
if (HappyFace[INSTANCE]) return HappyFace[INSTANCE];
function smile() { .. }
return HappyFace[INSTANCE] = {
smile: smile
};
}
var me = HappyFace(),
you = HappyFace();
me === you; // true
```
這里的symbol值`INSTANCE`是一個被靜態地存儲在`HappyFace()`函數對象上的特殊的,幾乎是隱藏的,類元屬性。
替代性地,它本可以是一個像`__instance`這樣的普通屬性,而且其行為將會是一模一樣的。symbol的使用僅僅增強了程序元編程的風格,將這個`INSTANCE`屬性與其他普通的屬性間保持隔離。
### Symbol注冊表
在前面幾個例子中使用symbol的一個微小的缺點是,變量`EVT_LOGIN`和`INSTANCE`不得不存儲在外部作用域中(甚至也許是全局作用域),或者用某種方法存儲在一個可用的公共位置,這樣代碼所有需要使用這些symbol的部分都可以訪問它們。
為了輔助組織訪問這些symbol的代碼,你可以使用?*全局symbol注冊表*?來創建symbol。例如:
```source-js
const EVT_LOGIN = Symbol.for( "event.login" );
console.log( EVT_LOGIN ); // Symbol(event.login)
```
和:
```source-js
function HappyFace() {
const INSTANCE = Symbol.for( "instance" );
if (HappyFace[INSTANCE]) return HappyFace[INSTANCE];
// ..
return HappyFace[INSTANCE] = { .. };
}
```
`Symbol.for(..)`查詢全局symbol注冊表來查看一個symbol是否已經使用被提供的說明文本存儲過了,如果有就返回它。如果沒有,就創建一個并返回。換句話說,全局symbol注冊表通過描述文本將symbol值看作它們本身的單例。
但這也意味著只要使用匹配的描述名,你的應用程序的任何部分都可以使用`Symbol.for(..)`從注冊表中取得symbol。
諷刺的是,基本上symbol的本意是在你的應用程序中取代?*魔法字符串*?的使用(被賦予了特殊意義的隨意的字符串值)。但是你正是在全局symbol注冊表中使用?*魔法*?描述字符串值來唯一識別/定位它們的!
為了避免意外的沖突,你可能想使你的symbol描述十分獨特。這么做的一個簡單的方法是在它們之中包含前綴/環境/名稱空間的信息。
例如,考慮一個像下面這樣的工具:
```source-js
function extractValues(str) {
var key = Symbol.for( "extractValues.parse" ),
re = extractValues[key] ||
/[^=&]+?=([^&]+?)(?=&|$)/g,
values = [], match;
while (match = re.exec( str )) {
values.push( match[1] );
}
return values;
}
```
我們使用魔法字符串值`"extractValues.parse"`,因為在注冊表中的其他任何symbol都不太可能與這個描述相沖突。
如果這個工具的一個用戶想要覆蓋這個解析用的正則表達式,他們也可以使用symbol注冊表:
```source-js
extractValues[Symbol.for( "extractValues.parse" )] =
/..some pattern../g;
extractValues( "..some string.." );
```
除了symbol注冊表在全局地存儲這些值上提供的協助以外,我們在這里看到的一切其實都可以通過將魔法字符串`"extractValues.parse"`作為一個鍵,而不是一個symbol,來做到。這其中在元編程的層次上的改進要多于在函數層次上的改進。
你可能偶然會使用一個已經被存儲在注冊表中的symbol值來查詢它底層存儲了什么描述文本(鍵)。例如,因為你無法傳遞symbol值本身,你可能需要通知你的應用程序的另一個部分如何在注冊表中定位一個symbol。
你可以使用`Symbol.keyFor(..)`取得一個被注冊的symbol描述文本(鍵):
```source-js
var s = Symbol.for( "something cool" );
var desc = Symbol.keyFor( s );
console.log( desc ); // "something cool"
// 再次從注冊表取得symbol
var s2 = Symbol.for( desc );
s2 === s; // true
```
### Symbols作為對象屬性
如果一個symbol被用作一個對象的屬性/鍵,它會被以一種特殊的方式存儲,以至這個屬性不會出現在這個對象屬性的普通枚舉中:
```source-js
var o = {
foo: 42,
[ Symbol( "bar" ) ]: "hello world",
baz: true
};
Object.getOwnPropertyNames( o ); // [ "foo","baz" ]
```
要取得對象的symbol屬性:
```source-js
Object.getOwnPropertySymbols( o ); // [ Symbol(bar) ]
```
這表明一個屬性symbol實際上不是隱藏的或不可訪問的,因為你總是可以在`Object.getOwnPropertySymbols(..)`的列表中看到它。
#### 內建Symbols
ES6帶來了好幾種預定義的內建symbol,它們暴露了在JavaScript對象值上的各種元行為。然而,正如人們所預料的那樣,這些symbol?*沒有*?沒被注冊到全局symbol注冊表中。
取而代之的是,它們作為屬性被存儲到了`Symbol`函數對象中。例如,在本章早先的“`for..of`”一節中,我們介紹了值`Symbol.iterator`:
```source-js
var a = [1,2,3];
a[Symbol.iterator]; // native function
```
語言規范使用`@@`前綴注釋指代內建的symbol,最常見的幾個是:`@@iterator`,`@@toStringTag`,`@@toPrimitive`。還定義了幾個其他的symbol,雖然他們可能不那么頻繁地被使用。
注意:?關于這些內建symbol如何被用于元編程的詳細信息,參見第七章的“通用Symbol”。
## 復習
ES6給JavaScript增加了一堆新的語法形式,有好多東西要學!
這些東西中的大多數都是為了緩解常見編程慣用法中的痛點而設計的,比如為函數參數設置默認值和將“剩余”的參數收集到一個數組中。解構是一個強大的工具,用來更簡約地表達從數組或嵌套對象的賦值。
雖然像箭頭函數`=>`這樣的特性看起來也都是關于更簡短更好看的語法,但是它們實際上擁有非常特殊的行為,你應當在恰當的情況下有意地使用它們。
擴展的Unicode支持,新的正則表達式技巧,和新的`symbol`基本類型充實了ES6語法的發展演變。