第二章:語(yǔ)法 (1/5) -《你不知道的JavaScript:ES6 & Beyond》

如果寫(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聲明里包含bc兩個(gè)塊作用域變量弟断,for循環(huán)中包含ij兩個(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ō)扛点,這樣做足夠顯式了,也更有用岂丘。

letfor..infor..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可以forfor..infor..of循環(huán)的變量聲明中(見(jiàn)《for..of循環(huán)》一章)机隙。然而蜘拉,任何重新賦值的嘗試,都會(huì)拋錯(cuò)有鹿,例如for循環(huán)中的典型i++語(yǔ)句旭旭。

const,用還是不用葱跋?

這里也有一些傳言持寄,在某些特定的場(chǎng)景下源梭,JS引擎對(duì)const的優(yōu)化可能比letvar更好。理論上稍味,引擎如果知道哪些變量的值/類型不會(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)賦值為1y賦值為2纯衍,所以剩下的3栋齿、45就被放在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ù)組(你把它命名成什么都可以,很多人喜歡把它叫做rrest)卦绣,我們?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 == 11x !== 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》,本章原文在此蔓搞。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末胰丁,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子喂分,更是在濱河造成了極大的恐慌锦庸,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,366評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蒲祈,死亡現(xiàn)場(chǎng)離奇詭異甘萧,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)梆掸,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,521評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)幔嗦,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人沥潭,你說(shuō)我怎么就攤上這事℃业玻” “怎么了钝鸽?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,689評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)庞钢。 經(jīng)常有香客問(wèn)我拔恰,道長(zhǎng),這世上最難降的妖魔是什么基括? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,925評(píng)論 1 295
  • 正文 為了忘掉前任颜懊,我火速辦了婚禮,結(jié)果婚禮上风皿,老公的妹妹穿的比我還像新娘河爹。我一直安慰自己,他們只是感情好桐款,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,942評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布咸这。 她就那樣靜靜地躺著,像睡著了一般魔眨。 火紅的嫁衣襯著肌膚如雪媳维。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,727評(píng)論 1 305
  • 那天遏暴,我揣著相機(jī)與錄音侄刽,去河邊找鬼。 笑死朋凉,一個(gè)胖子當(dāng)著我的面吹牛州丹,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播侥啤,決...
    沈念sama閱讀 40,447評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼当叭,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼茬故!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起蚁鳖,我...
    開(kāi)封第一講書(shū)人閱讀 39,349評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤磺芭,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后醉箕,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體钾腺,經(jīng)...
    沈念sama閱讀 45,820評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,990評(píng)論 3 337
  • 正文 我和宋清朗相戀三年讥裤,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了放棒。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,127評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡己英,死狀恐怖间螟,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情损肛,我是刑警寧澤厢破,帶...
    沈念sama閱讀 35,812評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站治拿,受9級(jí)特大地震影響摩泪,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜劫谅,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,471評(píng)論 3 331
  • 文/蒙蒙 一见坑、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧捏检,春花似錦荞驴、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,017評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至冤狡,卻和暖如春孙蒙,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背悲雳。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,142評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工挎峦, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人合瓢。 一個(gè)月前我還...
    沈念sama閱讀 48,388評(píng)論 3 373
  • 正文 我出身青樓坦胶,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子顿苇,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,066評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • 特別說(shuō)明峭咒,為便于查閱,文章轉(zhuǎn)自https://github.com/getify/You-Dont-Know-JS...
    殺破狼real閱讀 568評(píng)論 0 0
  • 官方中文版原文鏈接 感謝社區(qū)中各位的大力支持纪岁,譯者再次奉上一點(diǎn)點(diǎn)福利:阿里云產(chǎn)品券凑队,享受所有官網(wǎng)優(yōu)惠,并抽取幸運(yùn)大...
    HetfieldJoe閱讀 3,037評(píng)論 3 37
  • 解構(gòu)(Destructuring) ES6引入了一個(gè)新的語(yǔ)法特性幔翰,叫做解構(gòu)漩氨,這可能會(huì)和結(jié)構(gòu)化賦值的概念有點(diǎn)混淆。為...
    怪物絡(luò)繹閱讀 470評(píng)論 0 0
  • 最近由于和老公的關(guān)系出現(xiàn)沖突遗增,所以情緒難以控制叫惊,很容易爆發(fā),這也影響了孩子的情緒做修。原來(lái)我對(duì)自己的老公一直信任霍狰,而且...
    糊兔兔閱讀 159評(píng)論 0 0
  • 科研思路: 大致方向:遙感圖像+超分 科研安排:前期調(diào)研,中期ideas+實(shí)驗(yàn)循環(huán)饰及,后期成文蚓耽。 階段調(diào)研總結(jié): 遙...
    熊大狀閱讀 498評(píng)論 0 0