感謝社區中各位的大力支持,譯者再次奉上一點點福利:阿里云產品券,享受所有官網優惠,并抽取幸運大獎:點擊這里領取
對象字面量擴展
ES6給不起眼兒的{ .. }
對象字面量增加了幾個重要的便利擴展。
簡約屬性
你一定很熟悉用這種形式的對象字面量聲明:
var x = 2, y = 3,
o = {
x: x,
y: y
};
如果到處說x: x
總是讓你感到繁冗,那么有個好消息。如果你需要定義一個名稱和詞法標識符一致的屬性,你可以將它從x: x
縮寫為x
。考慮如下代碼:
var x = 2, y = 3,
o = {
x,
y
};
簡約方法
本著與我們剛剛檢視的簡約屬性相同的精神,添附在對象字面量屬性上的函數也有一種便利簡約形式。
以前的方式:
var o = {
x: function(){
// ..
},
y: function(){
// ..
}
}
而在ES6中:
var o = {
x() {
// ..
},
y() {
// ..
}
}
警告: 雖然x() { .. }
看起來只是x: function(){ .. }
的縮寫,但是簡約方法有一種特殊行為,是它們對應的老方式所不具有的;確切地說,是允許super
(參見本章稍后的“對象super
”)的使用。
Generator(見第四章)也有一種簡約方法形式:
var o = {
*foo() { .. }
};
簡約匿名
雖然這種便利縮寫十分誘人,但是這其中有一個微妙的坑要小心。為了展示這一點,讓我們檢視一下如下的前ES6代碼,你可能會試著使用簡約方法來重構它:
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;
}
} );
這段蠢代碼只是生成兩個隨機數,然后用大的減去小的。但這里重要的不是它做的是什么,而是它是如何被定義的。讓我把焦點放在對象字面量和函數定義上,就像我們在這里看到的:
runSomething( {
something: function something(x,y) {
// ..
}
} );
為什么我們同時說something:
和function something
?這不是冗余嗎?實際上,不是,它們倆被用于不同的目的。屬性something
讓我們能夠調用o.something(..)
,有點兒像它的公有名稱。但是第二個something
是一個詞法名稱,使這個函數可以為了遞歸而從內部引用它自己。
你能看出來為什么return something(y,x)
這一行需要名稱something
來引用這個函數嗎?因為這里沒有對象的詞法名稱,要是有的話我們就可以說return o.something(y,x)
或者其他類似的東西。
當一個對象字面量的確擁有一個標識符名稱時,這其實是一個很常見的做法,比如:
var controller = {
makeRequest: function(..){
// ..
controller.makeRequest(..);
}
};
這是個好主意嗎?也許是,也許不是。你在假設名稱controller
將總是指向目標對象。但它也很可能不是 —— 函數makeRequest(..)
不能控制外部的代碼,因此不能強制你的假設一定成立。這可能會回過頭來咬到你。
另一些人喜歡使用this
定義這樣的東西:
var controller = {
makeRequest: function(..){
// ..
this.makeRequest(..);
}
};
這看起來不錯,而且如果你總是用controller.makeRequest(..)
來調用方法的話它就應該能工作。但現在你有一個this
綁定的坑,如果你做這樣的事情的話:
btn.addEventListener( "click", controller.makeRequest, false );
當然,你可以通過傳遞controller.makeRequest.bind(controller)
作為綁定到事件上的處理器引用來解決這個問題。但是這很討厭 —— 它不是很吸引人。
或者要是你的內部this.makeRequest(..)
調用需要從一個嵌套的函數內發起呢?你會有另一個this
綁定災難,人們經常使用var self = this
這種用黑科技解決,就像:
var controller = {
makeRequest: function(..){
var self = this;
btn.addEventListener( "click", function(){
// ..
self.makeRequest(..);
}, false );
}
};
更討厭。
注意: 更多關于this
綁定規則和陷阱的信息,參見本系列的 this與對象原型 的第一到二章。
好了,這些與簡約方法有什么關系?回想一下我們的something(..)
方法定義:
runSomething( {
something: function something(x,y) {
// ..
}
} );
在這里的第二個something
提供了一個超級便利的詞法標識符,它總是指向函數自己,給了我們一個可用于遞歸,事件綁定/解除等等的完美引用 —— 不用亂搞this
或者使用不可靠的對象引用。
太好了!
那么,現在我們試著將函數引用重構為這種ES6解約方法的形式:
runSomething( {
something(x,y) {
if (x > y) {
return something( y, x );
}
return y - x;
}
} );
第一眼看上去不錯,除了這個代碼將會壞掉。return something(..)
調用經不會找到something
標識符,所以你會得到一個ReferenceError
。噢,但為什么?
上面的ES6代碼段將會被翻譯為:
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的向前發展而變得有用得多。
考慮如下代碼:
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) { .. }
)。
計算型屬性名
你可能曾經遇到過像下面的代碼段那樣的情況,你的一個或多個屬性名來自于某種表達式,因此你不能將它們放在對象字面量中:
var prefix = "user_";
var o = {
baz: function(..){ .. }
};
o[ prefix + "foo" ] = function(..){ .. };
o[ prefix + "bar" ] = function(..){ .. };
..
ES6為對象字面定義增加了一種語法,它允許你指定一個應當被計算的表達式,其結果就是被賦值屬性名。考慮如下代碼:
var prefix = "user_";
var o = {
baz: function(..){ .. },
[ prefix + "foo" ]: function(..){ .. },
[ prefix + "bar" ]: function(..){ .. }
..
};
任何合法的表達式都可以出現在位于對象字面定義的屬性名位置的[ .. ]
內部。
很有可能,計算型屬性名最經常與Symbol
(我們將在本章稍后的“Symbol”中講解)一起使用,比如:
var o = {
[Symbol.toStringTag]: "really cool thing",
..
};
Symbol.toStringTag
是一個特殊的內建值,我們使用[ .. ]
語法求值得到,所以我們可以將值"really cool thing"
賦值給這個特殊的屬性名。
計算型屬性名還可以作為簡約方法或簡約generator的名稱出現:
var o = {
["f" + "oo"]() { .. } // 計算型簡約方法
*["b" + "ar"]() { .. } // 計算型簡約generator
};
設置[[Prototype]]
我們不會在這里講解原型的細節,所以關于它的更多信息,參見本系列的 this與對象原型。
有時候在你聲明對象字面量的同時給它的[[Prototype]]
賦值很有用。下面的代碼在一段時期內曾經是許多JS引擎的一種非標準擴展,但是在ES6中得到了標準化:
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(..)
。考慮如下代碼:
var o1 = {
// ..
};
var o2 = {
// ..
};
Object.setPrototypeOf( o2, o1 );
注意: 我們將在第六章中再次討論Object
。“Object.setPrototypeOf(..)
靜態函數”提供了關于Object.setPrototypeOf(..)
的額外細節。另外參見“Object.assign(..)
靜態函數”來了解另一種將o2
原型關聯到o1
的形式。
對象super
super
通常被認為是僅與類有關。然而,由于JS對象僅有原型而沒有類的性質,super
是同樣有效的,而且在普通對象的簡約方法中行為幾乎一樣。
考慮如下代碼:
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方式:
var name = "Kyle";
var greeting = "Hello " + name + "!";
console.log( greeting ); // "Hello Kyle!"
console.log( typeof greeting ); // "string"
現在,考慮這種新的ES6方式:
var name = "Kyle";
var greeting = `Hello ${name}!`;
console.log( greeting ); // "Hello Kyle!"
console.log( typeof greeting ); // "string"
如你所見,我們在一系列被翻譯為字符串字面量的字符周圍使用了`..`
,但是${..}
形式中的任何表達式都將立即內聯地被解析和求值。稱呼這樣的解析和求值的高大上名詞就是 插值(interpolation)(比模板要準確多了)。
被插值的字符串字面量表達式的結果只是一個老式的普通字符串,賦值給變量greeting
。
警告: typeof greeting == "string"
展示了為什么不將這些實體考慮為特殊的模板值很重要,因為你不能將這種字面量的未求值形式賦值給某些東西并復用它。`..`
字符串字面量在某種意義上更像是IIFE,因為它自動內聯地被求值。`..`
字符串字面量的結果只不過是一個簡單的字符串。
插值型字符串字面量的一個真正的好處是他們允許被分割為多行:
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文件中時才會發生。
插值表達式
在一個插值型字符串字面量中,任何合法的表達式都被允許出現在${..}
內部,包括函數調用,內聯函數表達式調用,甚至是另一個插值型字符串字面量!
考慮如下代碼:
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有些相像,事實上這也可以考慮為作用域行為的一種解釋。
考慮如下代碼:
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提供的更酷的特性。它可能看起來有點兒奇怪,而且也許一開始看起來一般不那么實用。但一旦你花些時間在它上面,標簽型字符串字面量的用處可能會令你驚訝。
例如:
function foo(strings, ...values) {
console.log( strings );
console.log( values );
}
var desc = "awesome";
foo`Everything is ${desc}!`;
// [ "Everything is ", "!"]
// [ "awesome" ]
讓我們花點兒時間考慮一下前面的代碼段中發生了什么。首先,跳出來的最刺眼的東西就是foo`Everything...`;
。它看起來不像是任何我們曾經見過的東西。不是嗎?
它實質上是一種不需要( .. )
的特殊函數調用。標簽 —— 在字符串字面量`..`
之前的foo
部分 —— 是一個應當被調用的函數的值。實際上,它可以是返回函數的任何表達式,甚至是一個返回另一個函數的函數調用,就像:
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(..)
)應當計算一個恰當的字符串值并返回它,所以你可以使用標簽型字符串字面量作為一個未打標簽的字符串字面量來使用:
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
拼接/穿插在一起。
那么實際的用法是什么?有許多高級的用法超出了我們要在這里討論的范圍。但這里有一個格式化美元數字的簡單想法(有些像基本的本地化):
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
屬性訪問這些原始字符串值,就像這樣:
function showraw(strings, ...values) {
console.log( strings );
console.log( strings.raw );
}
showraw`Hello\nWorld`;
// [ "Hello
// World" ]
// [ "Hello\nWorld" ]
原始版本的值保留了原始的轉義序列\n
(\
和n
是兩個分離的字符),但處理過的版本認為它是一個單獨的換行符。但是,早先提到的行終結符泛化操作,是對兩個值都實施的。
ES6帶來了一個內建函數,它可以用做字符串字面量的標簽:String.raw(..)
。它簡單地直通strings
值的原始版本:
console.log( `Hello\nWorld` );
// Hello
// World
console.log( String.raw`Hello\nWorld` );
// Hello\nWorld
String.raw`Hello\nWorld`.length;
// 12
字符串字面量標簽的其他用法包括國際化,本地化,和許多其他的特殊處理。
箭頭函數
我們在本章早先接觸了函數中this
綁定的復雜性,而且在本系列的 this與對象原型 中也以相當的篇幅講解過。理解普通函數中基于this
的編程帶來的挫折是很重要的,因為這是ES6的新=>
箭頭函數的主要動機。
作為與普通函數的比較,我們首先來展示一下箭頭函數看起來什么樣:
function foo(x,y) {
return x + y;
}
// 對比
var foo = (x,y) => x + y;
箭頭函數的定義由一個參數列表(零個或多個參數,如果參數不是只有一個,需要有一個( .. )
包圍這些參數)組成,緊跟著是一個=>
符號,然后是一個函數體。
所以,在前面的代碼段中,箭頭函數只是(x,y) => x + y
這一部分,而這個函數的引用剛好被賦值給了變量foo
。
函數體僅在含有多于一個表達式,或者由一個非表達式語句組成時才需要用{ .. }
括起來。如果僅含有一個表達式,而且你省略了外圍的{ .. }
,那么在這個表達式前面就會有一個隱含的return
,就像前面的代碼段中展示的那樣。
這里是一些其他種類的箭頭函數:
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的文獻(除了這個系列中的書目)看起來都立即將箭頭函數僅僅認作“新函數”。
這說明在關于箭頭函數的討論中,幾乎所有的例子都是簡短的單語句工具,比如那些作為回調傳遞給各種工具的箭頭函數。例如:
var a = [1,2,3,4,5];
a = a.map( v => v * 2 );
console.log( a ); // [2,4,6,8,10]
在這些情況下,你的內聯函數表達式很適合這種在一個單獨語句中快速計算并返回結果的模式,對于更繁冗的function
關鍵字和語法來說箭頭函數確實看起來是一個很吸人,而且輕量的替代品。
大多數人看著這樣簡潔的例子都傾向于發出“哦……!啊……!”的感嘆,就像我想象中你剛剛做的那樣!
然而我要警示你的是,在我看來,使用箭頭函數的語法代替普通的,多語句函數,特別是那些可以被自然地表達為函數聲明的函數,是某種誤用。
回憶本章早前的字符串字面量標簽函數dollabillsyall(..)
—— 讓我們將它改為使用=>
語法:
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
敏感的編碼中的一個痛點。
節省擊鍵是掩人耳目的東西,至多是一個誤導人的配角。
讓我們重溫本章早前的另一個例子:
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
將會不出所料地成為我們希望它成為的東西。
考慮如下代碼:
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
敏感的函數會發生么?你可能會猜到它將會把事情搞砸。沒錯。
考慮如下代碼:
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,怪異的代碼,或者錯誤。
不要相信任何說=>
主要是,或者幾乎是,為了減少幾下擊鍵的炒作。無論你是省下還是浪費了這幾下擊鍵,你都應當確切地知道你打入的每個字母是為了做什么。
提示: 如果你有一個函數,由于上述各種清楚的原因而不適合成為一個=>
箭頭函數,但同時它又被聲明為一個對象字面量的一部分,那么回想一下本章早先的“簡約方法”,它有簡短函數語法的另一種選擇。
對于如何/為何選用一個箭頭函數,如果你喜歡一個可視化的決策圖的話:
[圖片上傳失敗...(image-27a646-1515411105375)]
for..of
循環
伴隨著我們熟知的JavaScriptfor
和for..in
循環,ES6增加了一個for..of
循環,它循環遍歷一組由一個 迭代器(iterator) 產生的值。
你使用for..of
循環遍歷的值必須是一個 可迭代對象(iterable),或者它必須是一個可以被強制轉換/封箱(參見本系列的 類型與文法)為一個可迭代對象的值。一個可迭代對象只不過是一個可以生成迭代器的對象,然后由循環使用這個迭代器。
讓我們比較for..of
與for..in
來展示它們的區別:
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版本:
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
等價物,它同時展示了手動迭代一個迭代器(見第三章的“迭代器”):
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
遍歷任何對象來得到我們定義的一組值。
這是如何遍歷一個基本類型的字符串中的字符:
for (var c of "hello") {
console.log( c );
}
// "h" "e" "l" "l" "o"
基本類型字符串"hello"
被強制轉換/封箱為等價的String
對象包裝器,它是默認就是一個可迭代對象。
在for (XYZ of ABC)..
中,XYZ
子句既可以是一個賦值表達式也可以是一個聲明,這與for
和for..in
中相同的子句一模一樣。所以你可以做這樣的事情:
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模式下,這個正則表達式范例被看作兩個分離的字符,但它仍然可以在一個含有"??"
字符的字符串中找到匹配,如果你試一下就會看到:
/??/.test( "??-clef" ); // true
重要的是匹配的長度。例如:
/^.-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)。
粘性標志
另一個加入ES6正則表達式的模式標志是y
,它經常被稱為“粘性模式(sticky mode)”。粘性 實質上意味著正則表達式在它開始時有一個虛擬的錨點,這個錨點使正則表達式僅以自己的lastIndex
屬性所指示的位置為起點進行匹配。
為了展示一下,讓我們考慮兩個正則表達式,第一個沒有使用粘性模式而第二個有:
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(..)
更新。
現在,讓我們試一下粘性模式的正則表達式:
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
。
考慮如下代碼:
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
能夠在一個字符串上由始至終地進行反復匹配的最實際場景。考慮如下代碼:
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
相對匹配的東西,就像這樣:
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)
。
考慮如下代碼:
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
,匹配就會失敗。考慮如下代碼:
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
屬性的內容中解析出來,就像這樣:
var re = /foo/ig;
re.toString(); // "/foo/ig"
var flags = re.toString().match( /\/([gim]*)$/ )[1];
flags; // "ig"
在ES6中,你現在可以直接得到這些值,使用新的flags
屬性:
var re = /foo/ig;
re.flags; // "gi"
雖然是個細小的地方,但是ES6規范要求表達式的標志位以"gimuy"
的順序羅列,無論原本的范例中是以什么順序指定的。這就是出現/ig
和"gi"
的區別的原因。
是的,標志位被指定和羅列的順序無所謂。
ES6的另一個調整是,如果你向構造器RegExp(..)
傳遞一個既存的正則表達式,它現在是flags
敏感的:
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中你可以在復制時覆蓋標志位。