如果寫(xiě)過(guò)一段時(shí)間的JS幅聘,很可能你對(duì)語(yǔ)法已經(jīng)非常熟悉了外里。JS確實(shí)有很多奇怪的地方,但總體來(lái)說(shuō)它的語(yǔ)法還是合理而直觀的永罚,并且JS吸取了其他語(yǔ)言的特點(diǎn)啤呼,因此有很多相似之處。
然而ES6增加了不少新的語(yǔ)法形式呢袱,需要一點(diǎn)時(shí)間來(lái)消化官扣。在這章里,我們來(lái)一同領(lǐng)略它帶來(lái)了什么新東西羞福。
提示:本文寫(xiě)作之時(shí)惕蹄,書(shū)中討論的部分特性已經(jīng)被各種瀏覽器實(shí)現(xiàn)了(Firefox、Chrome等等)治专,但其中一些只是部分實(shí)現(xiàn)卖陵,還有更多根本的沒(méi)有被實(shí)現(xiàn)。直接運(yùn)行這些例子可能會(huì)得到各式各樣的體驗(yàn)张峰。在這種情況下泪蔫,你可以嘗試一些轉(zhuǎn)譯器(transpiler),因?yàn)檫@些工具會(huì)覆蓋絕大部分特性喘批。ES6Fiddle是一個(gè)在線交互式的Babel轉(zhuǎn)譯器撩荣,它很棒、簡(jiǎn)單易用谤祖,你可以在上面直接使用ES6語(yǔ)法婿滓。
塊作用域聲明
你可能已經(jīng)意識(shí)到JavaScript中變量作用域的基本單位是function
。如果你需要?jiǎng)?chuàng)建一個(gè)塊作用域粥喜,最普遍的做法不是使用普通的函數(shù)凸主,而是創(chuàng)建一個(gè)立即執(zhí)行函數(shù)表達(dá)式(IIFE)。例如:
var a = 2;
(function IIFE(){
var a = 3;
console.log( a ); // 3
})();
console.log( a ); // 2
let
聲明
而現(xiàn)在额湘,我們可以為任何一個(gè)塊創(chuàng)建聲明卿吐,這種語(yǔ)法毫無(wú)意外地叫做塊作用域旁舰。這意味著我們只需要一對(duì)花括號(hào){ .. }
就可以創(chuàng)建一個(gè)作用域。同時(shí)嗡官,var
是眾所周知地總是把變量綁定在外圍函數(shù)域(或者全局箭窜,如果是頂層),我們可以用let
來(lái)替代它:
var a = 2;
{
let a = 3;
console.log( a ); // 3
}
console.log( a ); // 2
在JS里衍腥,憑空使用一對(duì)花括號(hào){ .. }
還是挺少見(jiàn)的磺樱,也不太符合這門(mén)語(yǔ)言的習(xí)慣,但它還是有用的婆咸。從那些有塊作用域語(yǔ)言遷徙過(guò)來(lái)的開(kāi)發(fā)者可以很容易地能識(shí)別出這種模式竹捉。
我認(rèn)為用一個(gè)專職的{ .. }
是創(chuàng)建塊作用域內(nèi)的變量最好的方式。此外尚骄,你應(yīng)該總是把let
聲明放在那段代碼的最頂部块差。如果有多個(gè)變量需要聲明,我建議只寫(xiě)一個(gè)let
倔丈。
從代碼風(fēng)格上來(lái)說(shuō)憨闰,我甚至喜歡把let
和開(kāi)頭的{
放在同一行,讓別人一眼就能看出來(lái)這段代碼就是為了把這些變量包起來(lái)需五。
{ let a = 2, b, c;
// ..
}
現(xiàn)在開(kāi)始看起來(lái)有點(diǎn)怪了鹉动,而且也不太會(huì)是其他ES6文檔推薦的做法。但是我發(fā)這個(gè)瘋不是沒(méi)有理由的宏邮。
還有一個(gè)實(shí)驗(yàn)性(未標(biāo)準(zhǔn)化的)方式來(lái)使用let
聲明训裆,叫做let
塊,它看起來(lái)是這樣的:
let (a = 2, b, c) {
// ..
}
這種形式就是我所說(shuō)的顯示塊作用域蜀铲,鑒于let..
聲明形式映射出了var
是更隱式的棒拂,因?yàn)樗孟窠俪至怂诘娜魏?code>{ .. }钮孵。一般來(lái)說(shuō),比起隱式機(jī)制開(kāi)發(fā)人員更喜歡顯示的吕粹,可以說(shuō)這就是其中一個(gè)例子族扰。
和前面兩段代碼形式相比厌丑,它們是很類似的,對(duì)我來(lái)說(shuō)渔呵,這兩種語(yǔ)法風(fēng)格都是合格的顯示塊作用域怒竿。但不幸的是,let (..) { .. }
這種最最顯示的形式扩氢,并沒(méi)有被ES6采納耕驰。它有可能這未來(lái)加入ES6,所以前一種形式是我們目前的最佳選擇了录豺,至少在我看來(lái)朦肘。
為了增強(qiáng)對(duì)let
聲明的隱式本性的認(rèn)識(shí)饭弓,我們來(lái)看下面的用法:
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
}
不要回頭看上面的代碼快速回答下面的問(wèn)題:哪些(個(gè))變量只存在于if
聲明里面?哪些(個(gè))只存在于for
循環(huán)里面媒抠?
答案是:if
聲明里包含b
和c
兩個(gè)塊作用域變量弟断,for
循環(huán)中包含i
和j
兩個(gè)塊作用域變量。
你需要想一會(huì)兒?jiǎn)幔?code>i沒(méi)有被加到包圍它的if
聲明范圍里趴生,你有沒(méi)有覺(jué)得驚訝阀趴?大腦的停頓和質(zhì)疑——我叫它“智商稅”,來(lái)源于let
機(jī)制不光對(duì)我們來(lái)說(shuō)是新東西苍匆,而且它還是隱式的刘急。
在最下面的let c = ..
聲明也暗藏玄機(jī)。和傳統(tǒng)的var
變量聲明不同锉桑,那些會(huì)綁定到外層的函數(shù)作用域上排霉,不管它在哪兒聲明的,let
聲明不會(huì)在初始時(shí)就綁定到塊作用域上民轴,而是直到語(yǔ)句出現(xiàn)才初始化攻柠。
在let
聲明/初始化之前就訪問(wèn)let
聲明的變量會(huì)引起錯(cuò)誤,然而使用var
聲明的變量時(shí)則沒(méi)有這個(gè)問(wèn)題(除了代碼風(fēng)格問(wèn)題以外)后裸。
考慮以下代碼:
{
console.log( a ); // undefined
console.log( b ); // ReferenceError!
var a;
let b;
}
警告:因?yàn)檫^(guò)早去訪問(wèn)let
聲明的引用會(huì)導(dǎo)致ReferenceError
錯(cuò)誤瑰钮,技術(shù)上被稱為暫時(shí)性死區(qū)(TDZ)錯(cuò)誤——表明你正在訪問(wèn)一個(gè)已經(jīng)聲明還但沒(méi)有初始化的變量。這不是唯一能見(jiàn)到TDZ錯(cuò)誤的地方——他們出現(xiàn)在ES6中的好幾處地方微驶。同時(shí)浪谴,注意“初始化”并不要求你顯式地在代碼里給變量賦一個(gè)值,像leb b;
這樣聲明就可以了因苹。變量如果在聲明時(shí)沒(méi)有賦值苟耻,那么它會(huì)被賦上undefined
,所以let b;
和let b = undefined;
是等價(jià)的扶檐。但無(wú)論你用不用顯式聲明凶杖,你都不能在let b
語(yǔ)句前面訪問(wèn)b
。
最后一點(diǎn)小麻煩:和沒(méi)有聲明的變量(或者聲明了的?钪)不一樣智蝠,對(duì)于不同的TDZ變量,typeof
表現(xiàn)也不一樣奈梳¤就澹看下面的例子:
{
// `a`沒(méi)有聲明
if (typeof a === "undefined") {
console.log( "cool" );
}
// `b`聲明了,但還在TDZ中
if (typeof b === "undefined") { // ReferenceError!
// ..
}
// ..
let b;
}
a
是沒(méi)有聲明的攘须,所以typeof
是唯一安全地檢查它是否存在的方法漆撞。但是typeof b
拋出了TDZ錯(cuò)誤,因?yàn)樵谙旅娴拇a中有一個(gè)let b
聲明。哦豁叫挟。
你現(xiàn)在應(yīng)該明白為什么我堅(jiān)持把所有let
聲明放在scope的最上面了吧艰匙。這樣可以完全避免因?yàn)檫^(guò)早訪問(wèn)變量引起的錯(cuò)誤,同時(shí)也讓聲明變得更加顯式抹恳,當(dāng)你讀到一段代碼的開(kāi)頭员凝,就能知道這里面有哪些變量。
你的代碼(if
語(yǔ)句奋献、while
循環(huán)等等)沒(méi)必要和作用域行為分享他們?cè)械男袨椤?/p>
代碼的顯式程度——維護(hù)什么樣的原則由你自己決定——這樣可以免去很多重構(gòu)時(shí)的頭痛健霹,也不會(huì)搬起石頭砸自己的腳。
注意:如果想要了解更多關(guān)于`let和塊作用域的信息瓶蚂,請(qǐng)參考本系列的第三章《作用域和閉包》糖埋。
let
+ for
我推薦的顯示let
聲明方法有一個(gè)例外的情況,就是let
出現(xiàn)在for
循環(huán)的頂部的時(shí)候窃这。究其原因可能比較微不足道瞳别,但我認(rèn)為這是ES6很重要的一個(gè)特性。
考慮如下代碼:
var funcs = [];
for (let i = 0; i < 5; i++) {
funcs.push( function(){
console.log( i );
} );
}
funcs[3](); // 3
在for
頂部的let i聲明
不僅僅為for
循環(huán)定義了一個(gè)i
杭攻,還為每次循環(huán)重新定義了一個(gè)新的i
祟敛。這也意味著每次迭代創(chuàng)建的閉包只會(huì)存在于內(nèi)部,正如你期待的那樣兆解。
如果你用同樣的代碼馆铁,只在for
頂部改用var i
,你就會(huì)在最后得到5
而不是3
锅睛,因?yàn)槲ㄒ坏囊粋€(gè)i
是作用于外部作用域的埠巨,而不是每次迭代都使用一個(gè)新的i
。
你也可以用更羅嗦的代碼完成同樣的事情:
var funcs = [];
for (var i = 0; i < 5; i++) {
let j = i;
funcs.push( function(){
console.log( j );
} );
}
funcs[3](); // 3
這里现拒,我們?cè)诿總€(gè)迭代內(nèi)部強(qiáng)制創(chuàng)建了一個(gè)新的j
辣垒,然后閉包就會(huì)正常工作了。我更傾向于前面的那種做法印蔬;這個(gè)附加的特殊能力也是我更支持for (let ..)
寫(xiě)法的原因乍构。雖然你可以說(shuō)這么做有點(diǎn)隱式,但對(duì)我來(lái)說(shuō)扛点,這樣做足夠顯式了,也更有用岂丘。
let
在for..in
和for..of
(見(jiàn)《for..of
循環(huán)》)循環(huán)中表現(xiàn)是一樣的陵究。
const
聲明
我們還有另外一種塊作用域聲明形式:const
,用它來(lái)創(chuàng)建常量奥帘。
可到底什么才是常量呢铜邮?常量是一種在初始值被設(shè)定之后只讀的變量。考慮如下代碼:
{
const a = 2;
console.log( a ); // 2
a = 3; // TypeError!
}
在聲明時(shí)賦值之后松蒜,你就不能去更改它所存儲(chǔ)的值了扔茅。const
聲明必須使用顯式的初始化。如果你希望有一個(gè)常量的值是undefined
秸苗,那么你必須聲明const a = undefined
來(lái)達(dá)到目的召娜。
常量并沒(méi)有限制值本身,而是限制了變量和值之間的賦值關(guān)系惊楼。換句話說(shuō)玖瘸,用const
賦予的值并不是固定不可改變的,它只是限制了我們不能再次對(duì)同一變量賦值檀咙。假設(shè)這個(gè)值有些復(fù)雜雅倒,例如一個(gè)對(duì)象或者數(shù)組,值的內(nèi)容本身是可以改變的弧可。
{
const a = [1,2,3];
a.push( 4 );
console.log( a ); // [1,2,3,4]
a = 42; // TypeError!
}
這個(gè)例子里蔑匣,變量a
并不是真的存儲(chǔ)了一個(gè)常量數(shù)組,而是存儲(chǔ)了那個(gè)數(shù)組的恒定引用棕诵。數(shù)組本身還是可以隨便更改的裁良。
警告:把對(duì)象或者數(shù)組賦值為常量意味著這個(gè)值不能被垃圾回收,直到這個(gè)常量的詞法作用域結(jié)束年鸳,因?yàn)檫@個(gè)引用永遠(yuǎn)不能被取消趴久。這也許就是你想要的,但如果你不是有意為之的搔确,就要小心了彼棍。
本質(zhì)上,const
聲明強(qiáng)制實(shí)施了我們多年來(lái)用代碼風(fēng)格作為信號(hào)的習(xí)慣:當(dāng)我們用全部大寫(xiě)字母作為一個(gè)變量名并為其賦值時(shí)膳算,我們會(huì)留心不去改變它座硕。var
賦值不會(huì)強(qiáng)制這一點(diǎn),但是現(xiàn)在我們有了const
賦值涕蜂,就可以幫你避免無(wú)意的改變了华匾。
const
可以在for
、for..in
和for..of
循環(huán)的變量聲明中(見(jiàn)《for..of
循環(huán)》一章)机隙。然而蜘拉,任何重新賦值的嘗試,都會(huì)拋錯(cuò)有鹿,例如for
循環(huán)中的典型i++
語(yǔ)句旭旭。
const
,用還是不用葱跋?
這里也有一些傳言持寄,在某些特定的場(chǎng)景下源梭,JS引擎對(duì)const
的優(yōu)化可能比let
和var
更好。理論上稍味,引擎如果知道哪些變量的值/類型不會(huì)改變废麻,它就可以減少一些不必要的追蹤。
無(wú)論const
在這里是否真的有用模庐,它有可能僅僅是我們的一廂情愿烛愧,但更重要的決定是你是否需要一致的行為。請(qǐng)記桌敌馈:源代碼最重要的作用是幫助我們清晰無(wú)誤的溝通屑彻,不僅是為你自己,更要為未來(lái)的自己和其他代碼合作者解釋清楚此時(shí)此刻寫(xiě)下這些代碼的你的意圖是什么顶吮。
有一些程序員喜歡總是用const
來(lái)聲明變量社牲,直到他們發(fā)現(xiàn)有需要更改這個(gè)變量值的時(shí)候再把聲明改回let
。這是個(gè)有趣的想法悴了,但這樣做并沒(méi)有明顯地提升代碼可讀性或者是自我解釋性搏恤。
很多人認(rèn)為這并不是一種真正的保護(hù),因?yàn)槿魏魏髞?lái)的程序員誰(shuí)想要改一下const
聲明的變量湃交,只需要不假思索地把const
改成let
就好了熟空。最好的情況也只是它可以防止無(wú)意的更改。但是問(wèn)題又來(lái)了搞莺,除了我們的直覺(jué)和理智以外息罗,好像并沒(méi)有清晰客觀的手段來(lái)判斷哪些是“意外”或者是需要預(yù)防的。對(duì)于強(qiáng)制類型也有類似的觀念才沧。
我的建議是:為了避免這種會(huì)引起混亂的代碼迈喉,只在你確定肯定以及一定不會(huì)改變變量值的時(shí)候使用const
。換句話說(shuō)温圆,不要依靠const
的代碼行為挨摸,而是使用工具來(lái)檢查代碼風(fēng)格,當(dāng)你的想法可以被稱作風(fēng)格的時(shí)候岁歉。
塊作用域函數(shù)
從ES6開(kāi)始得运,在塊內(nèi)部的函數(shù)定義現(xiàn)在只在作用域內(nèi)有效了。ES6之前锅移,規(guī)范并沒(méi)有這么主張熔掺,但很多實(shí)現(xiàn)都已經(jīng)是這樣了。所以現(xiàn)在規(guī)范只能面對(duì)現(xiàn)實(shí)非剃。
考慮
{
foo(); // 可以調(diào)用瞬女!
function foo() {
// ..
}
}
foo(); // ReferenceError
foo()
函數(shù)是在{ .. }
內(nèi)部定義的,從ES6開(kāi)始它就被綁定在作用域內(nèi)部了努潘,所以在塊外部不能使用它。但需要注意的是它在塊作用域內(nèi)部是被“提升”了的,這和let
聲明又不一樣疯坤,你不會(huì)在聲明之前調(diào)用它時(shí)得到TDZ(暫時(shí)性死區(qū))錯(cuò)誤报慕。
如果你以前就這么寫(xiě)代碼并且依賴于沒(méi)有塊作用域的行為,那么塊作用域的函數(shù)聲明可能對(duì)你來(lái)說(shuō)是個(gè)問(wèn)題压怠。
if (something) {
function foo() {
console.log( "1" );
}
}
else {
function foo() {
console.log( "2" );
}
}
foo(); // ??
ES6之前的環(huán)境眠冈,無(wú)論something
的值是多少,foo()
都會(huì)打印出"2"
菌瘫。因?yàn)閮蓚€(gè)函數(shù)的聲明都被提升到了塊外面蜗顽,所以第二個(gè)永遠(yuǎn)都會(huì)覆蓋第一個(gè)。
到了ES6雨让,最后一行就會(huì)直接拋ReferenceError
錯(cuò)誤了雇盖。
展開(kāi)和其余(Spread/Rest)
ES6引入了一個(gè)新的操作符...
,一般被叫做展開(kāi)(spread)或者其余(rest)操作符栖忠,取決于它使用的位置和方式崔挖。我們來(lái)看一個(gè)例子:
function foo(x,y,z) {
console.log( x, y, z );
}
foo( ...[1,2,3] ); // 1 2 3
當(dāng)...
用在一個(gè)數(shù)組前面的時(shí)候(實(shí)際上任何可迭代的數(shù)據(jù)類型前面,我們會(huì)在第三章詳細(xì)講)庵寞,它會(huì)把這個(gè)數(shù)組展開(kāi)成平鋪的值狸相。
你會(huì)經(jīng)常見(jiàn)到上面這種用法,把一個(gè)數(shù)組展開(kāi)作為函數(shù)的一組參數(shù)捐川。這里的...
基本就是apply(..)
的一種簡(jiǎn)寫(xiě)脓鹃,在ES6之前經(jīng)常見(jiàn)到:
foo.apply( null, [1,2,3] ); // 1 2 3
但是...
也可以用來(lái)在其他情況下展開(kāi)一個(gè)值,比如在另外一個(gè)數(shù)組聲明的里面:
var a = [2,3,4];
var b = [ 1, ...a, 5 ];
console.log( b ); // [1,2,3,4,5]
在這里...
替代的是concat(..)
古沥,它和[1].concat( a, [5] )
的作用是一樣的瘸右。
...
另一個(gè)比較常見(jiàn)的用法本質(zhì)上有相反的作用:它不展開(kāi)一個(gè)值,而是聚合一組值放到一個(gè)數(shù)組中渐白,比如:
function foo(x, y, ...z) {
console.log( x, y, z );
}
foo( 1, 2, 3, 4, 5 ); // 1 2 [3,4,5]
這段代碼中的...z
其實(shí)是“把其余的參數(shù)(如果有的話)放到一個(gè)叫z
的數(shù)組里面尊浓。因?yàn)?code>x已經(jīng)賦值為1
,y
賦值為2
纯衍,所以剩下的3
栋齿、4
和5
就被放在z
里面了。
如果你沒(méi)有命名任何參數(shù)襟诸,那么...
就會(huì)合并所有的參數(shù)瓦堵。
function foo(...args) {
console.log( args );
}
foo( 1, 2, 3, 4, 5); // [1,2,3,4,5]
注意: foo()
函數(shù)聲明中的...args
通常被稱為“其余參數(shù)”,因?yàn)槟阍谑占瘏?shù)的剩余部分歌亲。我傾向于使用聚合菇用,因?yàn)檫@更符合它做的事情,而不是它包含的東西陷揪。
這個(gè)用法最好的地方就是它提供了一個(gè)穩(wěn)妥的辦法來(lái)替代早就廢棄了的arguments
數(shù)組——事實(shí)上它連個(gè)真正的數(shù)組都不是惋鸥,而是一個(gè)看起來(lái)像數(shù)組的對(duì)象杂穷。因?yàn)?code>args是一個(gè)真正的數(shù)組(你把它命名成什么都可以,很多人喜歡把它叫做r
或rest
)卦绣,我們?cè)僖膊挥米龈鞣N愚蠢的事情去把arguments
轉(zhuǎn)成真正的數(shù)組了耐量。
考慮:
// 用ES6的方式來(lái)實(shí)現(xiàn)
function foo(...args) {
// `args`是真正的數(shù)組
// 丟掉`args`數(shù)組中的第一個(gè)元素
args.shift();
// 把所有`args`里的元素當(dāng)作參數(shù)傳給`console.log(..)`
console.log( ...args );
}
// 用ES6之前的老式辦法實(shí)現(xiàn)
function bar() {
// 先把`arguments`轉(zhuǎn)換為真的數(shù)組
var args = Array.prototype.slice.call( arguments );
// 在末尾加上幾個(gè)元素
args.push( 4, 5 );
// 篩掉奇數(shù)元素
args = args.filter( function(v){
return v % 2 == 0;
} );
// 把`args`里的元素當(dāng)作參數(shù)傳給`foo(..)`
foo.apply( null, args );
}
bar( 0, 1, 2, 3 ); // 2 4
foo(..)
函數(shù)中的...args
聚合了所有參數(shù),然后在調(diào)用console.log(..)
的時(shí)候...args
又把它們展開(kāi)了滤港。這個(gè)例子可以很好地體現(xiàn)...
操作符兩種相反的用法廊蜒。
...
用法除了可以用在函數(shù)聲明時(shí)使用以外還有其他的用途,我們會(huì)在《不多不少剛剛好》一章來(lái)詳細(xì)講述溅漾。
參數(shù)默認(rèn)值
給函數(shù)參數(shù)設(shè)一個(gè)默認(rèn)值恐怕是JavaScript里最常見(jiàn)的寫(xiě)法了山叮。長(zhǎng)久以來(lái)我們大概都是這么做的,你可能對(duì)下面的代碼似曾相識(shí):
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
當(dāng)然添履,如果以前這樣做過(guò)屁倔,你可能已經(jīng)知道這樣做有利有弊,比如在參數(shù)值和false
等價(jià)的時(shí)候:
foo( 0, 42 ); // 53 <-- 哦豁缝龄,不是42喔汰现。
為什么會(huì)這樣?因?yàn)?code>0是一個(gè)假值叔壤,所以x || 11
會(huì)得到11
瞎饲,而不會(huì)把0
傳過(guò)去。
為了避免這個(gè)小意外炼绘,有些人會(huì)用稍顯羅嗦的辦法來(lái)實(shí)現(xiàn):
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
當(dāng)然嗅战,這么做意味著除了undefined
以外的任何值都可以傳進(jìn)去。然而俺亮,undefined
會(huì)被當(dāng)成信號(hào)驮捍,表示“我沒(méi)有傳這個(gè)參數(shù)”。這么做沒(méi)有問(wèn)題脚曾,除非哪天你真的需要把undefined
傳進(jìn)去东且。
如果是這樣,你可以測(cè)試這個(gè)參數(shù)是否真的被忽略了本讥,通過(guò)檢查它是否出現(xiàn)在了arguments
數(shù)組里珊泳,代碼可能是這樣的:
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
但是你怎么可能不傳點(diǎn)什么值標(biāo)明“我要跳過(guò)這個(gè)參數(shù)”(連undefined
都不傳),就跳過(guò)第一個(gè)參數(shù)x
拷沸?
foo(,5)
看起來(lái)不錯(cuò)色查,但這是個(gè)語(yǔ)法錯(cuò)誤。foo.apply(null,[,5])
看起來(lái)好像可以工作撞芍,但apply(..)
有個(gè)怪癖秧了,這樣的參數(shù)會(huì)被當(dāng)成[undefined,5]
,也就是說(shuō)也不會(huì)被跳過(guò)的序无。
如果你深挖下去验毡,你會(huì)發(fā)現(xiàn)你只能省略末尾的參數(shù)(即右邊的)衡创,只需要比所需的參數(shù)少傳幾個(gè)就可以了,但你不能跳過(guò)參數(shù)中間或者前面的幾個(gè)晶通。就是做不到钧汹。
JavaScript設(shè)計(jì)中有一個(gè)重要的原則就是undefined
通常意味著缺失。也就是說(shuō)undefined
和缺失之間是沒(méi)區(qū)別的录择,至少在函數(shù)參數(shù)這里是這樣的。
注意:比較混亂的是碗降,JS里也有很多不符合設(shè)計(jì)模式的地方隘竭,比如數(shù)組里的空位之類的。見(jiàn)本系列中的《類型和語(yǔ)法》一書(shū)讼渊。
鋪墊了這么多动看,我們現(xiàn)在來(lái)看一個(gè)新的ES6語(yǔ)法,它好看又實(shí)用爪幻,可以高效地幫我們給缺失的參數(shù)賦上默認(rèn)值菱皆。
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`不見(jiàn)了
foo( 5, null ); // 5 <-- `null`強(qiáng)制轉(zhuǎn)換成了`0`
foo( undefined, 6 ); // 17 <-- `undefined`不見(jiàn)了
foo( null, 6 ); // 6 <-- `null`強(qiáng)制轉(zhuǎn)換成了`0`
注意我們得到的輸出,看它們和之前做法的細(xì)微差別和相似之處挨稿。
比起慣用的x || 11
仇轻,函數(shù)聲明中的x == 11
和x !== undefined ? x : 11
的行為更相似,所以在把前ES6的代碼轉(zhuǎn)換成ES6的時(shí)候奶甘,你要格外小心這些有默認(rèn)值的語(yǔ)法篷店。
注意: 其余/聚合參數(shù)(見(jiàn)《展開(kāi)和其余》)不能設(shè)置默認(rèn)值。所以臭家,當(dāng)function foo(...vals=[1,2,3]) {
看起來(lái)好像很不錯(cuò)的樣子疲陕,但實(shí)際上這個(gè)語(yǔ)法并不成立。這種情況下你還是需要繼續(xù)手寫(xiě)前面的邏輯來(lái)實(shí)現(xiàn)钉赁。
默認(rèn)值表達(dá)式
函數(shù)的默認(rèn)值可以不僅僅是簡(jiǎn)單的像31
這種的值蹄殃;它們可以是任意有效的表達(dá)式,甚至可以是函數(shù)調(diào)用:
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
如你所見(jiàn)你踩,這些默認(rèn)值表達(dá)式是延遲執(zhí)行的诅岩,這意味著它們只在被調(diào)用的時(shí)候執(zhí)行——也就是,只有當(dāng)函數(shù)參數(shù)被省略或者是undefined
的時(shí)候姓蜂。
這是個(gè)很微妙的細(xì)節(jié)按厘,但是函數(shù)聲明里正式參數(shù)是在它們自己作用域里的(把它想象成一個(gè)用( .. )
包裹起來(lái)的作用域氣泡),而不是在函數(shù)體作用域里钱慢。這意味著在默認(rèn)值表達(dá)式里的對(duì)一個(gè)標(biāo)識(shí)符的引用會(huì)首先匹配正式的參數(shù)作用域逮京,然后才會(huì)去看外部的作用域。詳見(jiàn)《作用域和閉包》一書(shū)束莫。
參考:
var w = 1, z = 2;
function foo( x = w + 1, y = x + 1, z = z + 1 ) {
console.log( x, y, z );
}
foo(); // ReferenceError
默認(rèn)值表達(dá)式w + 1
中的w
會(huì)在正式的參數(shù)作用域中尋找w
懒棉,但是沒(méi)找到草描,它就會(huì)退而求其次來(lái)使用外部作用域中的w
。接下來(lái)策严,默認(rèn)值表達(dá)式x + 1
也會(huì)在正式的參數(shù)作用域中尋找x
穗慕,幸運(yùn)的是這時(shí)候x
已經(jīng)初始化了,所以y
的賦值也沒(méi)有問(wèn)題妻导。
然而逛绵,z + 1
里的z
就沒(méi)那么好運(yùn)了,它在那一刻還沒(méi)有初始化的變量倔韭,所以它也不會(huì)在外部作用域里費(fèi)力找z
术浪。
我們?cè)?code>let聲明一章已經(jīng)提到過(guò),ES6有TDZ(暫時(shí)性死區(qū))寿酌,會(huì)阻止我們?cè)L問(wèn)一個(gè)還沒(méi)初始化的變量胰苏。在這里,默認(rèn)值表達(dá)式z + 1
就會(huì)拋出一個(gè)TDZ錯(cuò)誤ReferenceError
醇疼。
盡管對(duì)代碼清晰度來(lái)說(shuō)并不是個(gè)好的主意硕并,但默認(rèn)值表達(dá)式確實(shí)可以寫(xiě)成一個(gè)inline的函數(shù)表達(dá)式調(diào)用——一般稱為立即執(zhí)行函數(shù)(IIFE):
function foo( x =
(function(v){ return v + 11; })( 31 )
) {
console.log( x );
}
foo(); // 42
這絕對(duì)不是默認(rèn)值表達(dá)式和立即執(zhí)行函數(shù)正常的相處方式,如果你發(fā)現(xiàn)你在朝這條路上走秧荆,那么請(qǐng)退一步重新審視一下自己為什么要這么做倔毙!
警告:如果立即執(zhí)行函數(shù)嘗試去訪問(wèn)標(biāo)識(shí)符x
,并且還沒(méi)有聲明自己的x
辰如,那么它也會(huì)得到一個(gè)TDZ錯(cuò)誤普监,和之前提到的一樣。
在前面代碼里的默認(rèn)值表達(dá)式是一個(gè)立即執(zhí)行函數(shù)琉兜,通過(guò)立即調(diào)用的(31)
執(zhí)行的凯正。如果不小心遺漏了這部分,那么x
的默認(rèn)值將是函數(shù)引用本身豌蟋,可能就像是一個(gè)回調(diào)函數(shù)廊散。這樣么做可能還比較有用,例如:
function ajax(url, cb = function(){}) {
// ..
}
ajax( "http://some.url.1" );
在這種情況下梧疲,我們非常希望沒(méi)賦值的cb
是空操作允睹,如果沒(méi)有任何其他的指令。這里的函數(shù)表達(dá)式只是一個(gè)引用幌氮,并不是函數(shù)調(diào)用本身(我們省略了末尾的()
)缭受,反而達(dá)到了我們的目的。
早期的JS里有個(gè)不太為人知但是確實(shí)有用的技巧:Function.prototype
本身就是一個(gè)空的無(wú)操作函數(shù)该互。所以上面的表達(dá)式可以寫(xiě)成cb = Function.prototype
米者,這樣就可以省掉函數(shù)表達(dá)式了。
該系列文章翻譯自Kyle Simpson的《You don't know about Javascript》,本章原文在此蔓搞。