如果寫過一段時間的JS,很可能你對語法已經(jīng)非常熟悉了。JS確實有很多奇怪的地方,但總體來說它的語法還是合理而直觀的,并且JS吸取了其他語言的特點,因此有很多相似之處。
然而ES6增加了不少新的語法形式,需要一點時間來消化。在這章里,我們來一同領(lǐng)略它帶來了什么新東西。
提示:本文寫作之時,書中討論的部分特性已經(jīng)被各種瀏覽器實現(xiàn)了(Firefox、Chrome等等),但其中一些只是部分實現(xiàn),還有更多根本的沒有被實現(xiàn)。直接運行這些例子可能會得到各式各樣的體驗。在這種情況下,你可以嘗試一些轉(zhuǎn)譯器(transpiler),因為這些工具會覆蓋絕大部分特性。ES6Fiddle是一個在線交互式的Babel轉(zhuǎn)譯器,它很棒、簡單易用,你可以在上面直接使用ES6語法。
塊作用域聲明
你可能已經(jīng)意識到JavaScript中變量作用域的基本單位是function
。如果你需要創(chuàng)建一個塊作用域,最普遍的做法不是使用普通的函數(shù),而是創(chuàng)建一個立即執(zhí)行函數(shù)表達(dá)式(IIFE)。例如:
var a = 2;
(function IIFE(){
var a = 3;
console.log( a ); // 3
})();
console.log( a ); // 2
let
聲明
而現(xiàn)在,我們可以為任何一個塊創(chuàng)建聲明,這種語法毫無意外地叫做塊作用域。這意味著我們只需要一對花括號{ .. }
就可以創(chuàng)建一個作用域。同時,var
是眾所周知地總是把變量綁定在外圍函數(shù)域(或者全局,如果是頂層),我們可以用let
來替代它:
var a = 2;
{
let a = 3;
console.log( a ); // 3
}
console.log( a ); // 2
在JS里,憑空使用一對花括號{ .. }
還是挺少見的,也不太符合這門語言的習(xí)慣,但它還是有用的。從那些有塊作用域語言遷徙過來的開發(fā)者可以很容易地能識別出這種模式。
我認(rèn)為用一個專職的{ .. }
是創(chuàng)建塊作用域內(nèi)的變量最好的方式。此外,你應(yīng)該總是把let
聲明放在那段代碼的最頂部。如果有多個變量需要聲明,我建議只寫一個let
。
從代碼風(fēng)格上來說,我甚至喜歡把let
和開頭的{
放在同一行,讓別人一眼就能看出來這段代碼就是為了把這些變量包起來。
{ let a = 2, b, c;
// ..
}
現(xiàn)在開始看起來有點怪了,而且也不太會是其他ES6文檔推薦的做法。但是我發(fā)這個瘋不是沒有理由的。
還有一個實驗性(未標(biāo)準(zhǔn)化的)方式來使用let
聲明,叫做let
塊,它看起來是這樣的:
let (a = 2, b, c) {
// ..
}
這種形式就是我所說的顯示塊作用域,鑒于let..
聲明形式映射出了var
是更隱式的,因為它好像劫持了它所在的任何{ .. }
。一般來說,比起隱式機制開發(fā)人員更喜歡顯示的,可以說這就是其中一個例子。
和前面兩段代碼形式相比,它們是很類似的,對我來說,這兩種語法風(fēng)格都是合格的顯示塊作用域。但不幸的是,let (..) { .. }
這種最最顯示的形式,并沒有被ES6采納。它有可能這未來加入ES6,所以前一種形式是我們目前的最佳選擇了,至少在我看來。
為了增強對let
聲明的隱式本性的認(rèn)識,我們來看下面的用法:
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
循環(huán)里面?
答案是:if
聲明里包含b
和c
兩個塊作用域變量,for
循環(huán)中包含i
和j
兩個塊作用域變量。
你需要想一會兒嗎?i
沒有被加到包圍它的if
聲明范圍里,你有沒有覺得驚訝?大腦的停頓和質(zhì)疑——我叫它“智商稅”,來源于let
機制不光對我們來說是新東西,而且它還是隱式的。
在最下面的let c = ..
聲明也暗藏玄機。和傳統(tǒng)的var
變量聲明不同,那些會綁定到外層的函數(shù)作用域上,不管它在哪兒聲明的,let
聲明不會在初始時就綁定到塊作用域上,而是直到語句出現(xiàn)才初始化。
在let
聲明/初始化之前就訪問let
聲明的變量會引起錯誤,然而使用var
聲明的變量時則沒有這個問題(除了代碼風(fēng)格問題以外)。
考慮以下代碼:
{
console.log( a ); // undefined
console.log( b ); // ReferenceError!
var a;
let b;
}
警告:因為過早去訪問let
聲明的引用會導(dǎo)致ReferenceError
錯誤,技術(shù)上被稱為暫時性死區(qū)(TDZ)錯誤——表明你正在訪問一個已經(jīng)聲明還但沒有初始化的變量。這不是唯一能見到TDZ錯誤的地方——他們出現(xiàn)在ES6中的好幾處地方。同時,注意“初始化”并不要求你顯式地在代碼里給變量賦一個值,像leb b;
這樣聲明就可以了。變量如果在聲明時沒有賦值,那么它會被賦上undefined
,所以let b;
和let b = undefined;
是等價的。但無論你用不用顯式聲明,你都不能在let b
語句前面訪問b
。
最后一點小麻煩:和沒有聲明的變量(或者聲明了的!)不一樣,對于不同的TDZ變量,typeof
表現(xiàn)也不一樣。看下面的例子:
{
// `a`沒有聲明
if (typeof a === "undefined") {
console.log( "cool" );
}
// `b`聲明了,但還在TDZ中
if (typeof b === "undefined") { // ReferenceError!
// ..
}
// ..
let b;
}
a
是沒有聲明的,所以typeof
是唯一安全地檢查它是否存在的方法。但是typeof b
拋出了TDZ錯誤,因為在下面的代碼中有一個let b
聲明。哦豁。
你現(xiàn)在應(yīng)該明白為什么我堅持把所有let
聲明放在scope的最上面了吧。這樣可以完全避免因為過早訪問變量引起的錯誤,同時也讓聲明變得更加顯式,當(dāng)你讀到一段代碼的開頭,就能知道這里面有哪些變量。
你的代碼(if
語句、while
循環(huán)等等)沒必要和作用域行為分享他們原有的行為。
代碼的顯式程度——維護(hù)什么樣的原則由你自己決定——這樣可以免去很多重構(gòu)時的頭痛,也不會搬起石頭砸自己的腳。
注意:如果想要了解更多關(guān)于`let和塊作用域的信息,請參考本系列的第三章《作用域和閉包》。
let
+ for
我推薦的顯示let
聲明方法有一個例外的情況,就是let
出現(xiàn)在for
循環(huán)的頂部的時候。究其原因可能比較微不足道,但我認(rèn)為這是ES6很重要的一個特性。
考慮如下代碼:
var funcs = [];
for (let i = 0; i < 5; i++) {
funcs.push( function(){
console.log( i );
} );
}
funcs[3](); // 3
在for
頂部的let i聲明
不僅僅為for
循環(huán)定義了一個i
,還為每次循環(huán)重新定義了一個新的i
。這也意味著每次迭代創(chuàng)建的閉包只會存在于內(nèi)部,正如你期待的那樣。
如果你用同樣的代碼,只在for
頂部改用var i
,你就會在最后得到5
而不是3
,因為唯一的一個i
是作用于外部作用域的,而不是每次迭代都使用一個新的i
。
你也可以用更羅嗦的代碼完成同樣的事情:
var funcs = [];
for (var i = 0; i < 5; i++) {
let j = i;
funcs.push( function(){
console.log( j );
} );
}
funcs[3](); // 3
這里,我們在每個迭代內(nèi)部強制創(chuàng)建了一個新的j
,然后閉包就會正常工作了。我更傾向于前面的那種做法;這個附加的特殊能力也是我更支持for (let ..)
寫法的原因。雖然你可以說這么做有點隱式,但對我來說,這樣做足夠顯式了,也更有用。
let
在for..in
和for..of
(見《for..of
循環(huán)》)循環(huán)中表現(xiàn)是一樣的。
const
聲明
我們還有另外一種塊作用域聲明形式:const
,用它來創(chuàng)建常量。
可到底什么才是常量呢?常量是一種在初始值被設(shè)定之后只讀的變量。考慮如下代碼:
{
const a = 2;
console.log( a ); // 2
a = 3; // TypeError!
}
在聲明時賦值之后,你就不能去更改它所存儲的值了。const
聲明必須使用顯式的初始化。如果你希望有一個常量的值是undefined
,那么你必須聲明const a = undefined
來達(dá)到目的。
常量并沒有限制值本身,而是限制了變量和值之間的賦值關(guān)系。換句話說,用const
賦予的值并不是固定不可改變的,它只是限制了我們不能再次對同一變量賦值。假設(shè)這個值有些復(fù)雜,例如一個對象或者數(shù)組,值的內(nèi)容本身是可以改變的。
{
const a = [1,2,3];
a.push( 4 );
console.log( a ); // [1,2,3,4]
a = 42; // TypeError!
}
這個例子里,變量a
并不是真的存儲了一個常量數(shù)組,而是存儲了那個數(shù)組的恒定引用。數(shù)組本身還是可以隨便更改的。
警告:把對象或者數(shù)組賦值為常量意味著這個值不能被垃圾回收,直到這個常量的詞法作用域結(jié)束,因為這個引用永遠(yuǎn)不能被取消。這也許就是你想要的,但如果你不是有意為之的,就要小心了。
本質(zhì)上,const
聲明強制實施了我們多年來用代碼風(fēng)格作為信號的習(xí)慣:當(dāng)我們用全部大寫字母作為一個變量名并為其賦值時,我們會留心不去改變它。var
賦值不會強制這一點,但是現(xiàn)在我們有了const
賦值,就可以幫你避免無意的改變了。
const
可以在for
、for..in
和for..of
循環(huán)的變量聲明中(見《for..of
循環(huán)》一章)。然而,任何重新賦值的嘗試,都會拋錯,例如for
循環(huán)中的典型i++
語句。
const
,用還是不用?
這里也有一些傳言,在某些特定的場景下,JS引擎對const
的優(yōu)化可能比let
和var
更好。理論上,引擎如果知道哪些變量的值/類型不會改變,它就可以減少一些不必要的追蹤。
無論const
在這里是否真的有用,它有可能僅僅是我們的一廂情愿,但更重要的決定是你是否需要一致的行為。請記住:源代碼最重要的作用是幫助我們清晰無誤的溝通,不僅是為你自己,更要為未來的自己和其他代碼合作者解釋清楚此時此刻寫下這些代碼的你的意圖是什么。
有一些程序員喜歡總是用const
來聲明變量,直到他們發(fā)現(xiàn)有需要更改這個變量值的時候再把聲明改回let
。這是個有趣的想法,但這樣做并沒有明顯地提升代碼可讀性或者是自我解釋性。
很多人認(rèn)為這并不是一種真正的保護(hù),因為任何后來的程序員誰想要改一下const
聲明的變量,只需要不假思索地把const
改成let
就好了。最好的情況也只是它可以防止無意的更改。但是問題又來了,除了我們的直覺和理智以外,好像并沒有清晰客觀的手段來判斷哪些是“意外”或者是需要預(yù)防的。對于強制類型也有類似的觀念。
我的建議是:為了避免這種會引起混亂的代碼,只在你確定肯定以及一定不會改變變量值的時候使用const
。換句話說,不要依靠const
的代碼行為,而是使用工具來檢查代碼風(fēng)格,當(dāng)你的想法可以被稱作風(fēng)格的時候。
塊作用域函數(shù)
從ES6開始,在塊內(nèi)部的函數(shù)定義現(xiàn)在只在作用域內(nèi)有效了。ES6之前,規(guī)范并沒有這么主張,但很多實現(xiàn)都已經(jīng)是這樣了。所以現(xiàn)在規(guī)范只能面對現(xiàn)實。
考慮
{
foo(); // 可以調(diào)用!
function foo() {
// ..
}
}
foo(); // ReferenceError
foo()
函數(shù)是在{ .. }
內(nèi)部定義的,從ES6開始它就被綁定在作用域內(nèi)部了,所以在塊外部不能使用它。但需要注意的是它在塊作用域內(nèi)部是被“提升”了的,這和let
聲明又不一樣,你不會在聲明之前調(diào)用它時得到TDZ(暫時性死區(qū))錯誤。
如果你以前就這么寫代碼并且依賴于沒有塊作用域的行為,那么塊作用域的函數(shù)聲明可能對你來說是個問題。
if (something) {
function foo() {
console.log( "1" );
}
}
else {
function foo() {
console.log( "2" );
}
}
foo(); // ??
ES6之前的環(huán)境,無論something
的值是多少,foo()
都會打印出"2"
。因為兩個函數(shù)的聲明都被提升到了塊外面,所以第二個永遠(yuǎn)都會覆蓋第一個。
到了ES6,最后一行就會直接拋ReferenceError
錯誤了。
展開和其余(Spread/Rest)
ES6引入了一個新的操作符...
,一般被叫做展開(spread)或者其余(rest)操作符,取決于它使用的位置和方式。我們來看一個例子:
function foo(x,y,z) {
console.log( x, y, z );
}
foo( ...[1,2,3] ); // 1 2 3
當(dāng)...
用在一個數(shù)組前面的時候(實際上任何可迭代的數(shù)據(jù)類型前面,我們會在第三章詳細(xì)講),它會把這個數(shù)組展開成平鋪的值。
你會經(jīng)常見到上面這種用法,把一個數(shù)組展開作為函數(shù)的一組參數(shù)。這里的...
基本就是apply(..)
的一種簡寫,在ES6之前經(jīng)常見到:
foo.apply( null, [1,2,3] ); // 1 2 3
但是...
也可以用來在其他情況下展開一個值,比如在另外一個數(shù)組聲明的里面:
var a = [2,3,4];
var b = [ 1, ...a, 5 ];
console.log( b ); // [1,2,3,4,5]
在這里...
替代的是concat(..)
,它和[1].concat( a, [5] )
的作用是一樣的。
...
另一個比較常見的用法本質(zhì)上有相反的作用:它不展開一個值,而是聚合一組值放到一個數(shù)組中,比如:
function foo(x, y, ...z) {
console.log( x, y, z );
}
foo( 1, 2, 3, 4, 5 ); // 1 2 [3,4,5]
這段代碼中的...z
其實是“把其余的參數(shù)(如果有的話)放到一個叫z
的數(shù)組里面。因為x
已經(jīng)賦值為1
,y
賦值為2
,所以剩下的3
、4
和5
就被放在z
里面了。
如果你沒有命名任何參數(shù),那么...
就會合并所有的參數(shù)。
function foo(...args) {
console.log( args );
}
foo( 1, 2, 3, 4, 5); // [1,2,3,4,5]
注意: foo()
函數(shù)聲明中的...args
通常被稱為“其余參數(shù)”,因為你在收集參數(shù)的剩余部分。我傾向于使用聚合,因為這更符合它做的事情,而不是它包含的東西。
這個用法最好的地方就是它提供了一個穩(wěn)妥的辦法來替代早就廢棄了的arguments
數(shù)組——事實上它連個真正的數(shù)組都不是,而是一個看起來像數(shù)組的對象。因為args
是一個真正的數(shù)組(你把它命名成什么都可以,很多人喜歡把它叫做r
或rest
),我們再也不用做各種愚蠢的事情去把arguments
轉(zhuǎn)成真正的數(shù)組了。
考慮:
// 用ES6的方式來實現(xiàn)
function foo(...args) {
// `args`是真正的數(shù)組
// 丟掉`args`數(shù)組中的第一個元素
args.shift();
// 把所有`args`里的元素當(dāng)作參數(shù)傳給`console.log(..)`
console.log( ...args );
}
// 用ES6之前的老式辦法實現(xiàn)
function bar() {
// 先把`arguments`轉(zhuǎn)換為真的數(shù)組
var args = Array.prototype.slice.call( arguments );
// 在末尾加上幾個元素
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(..)
的時候...args
又把它們展開了。這個例子可以很好地體現(xiàn)...
操作符兩種相反的用法。
...
用法除了可以用在函數(shù)聲明時使用以外還有其他的用途,我們會在《不多不少剛剛好》一章來詳細(xì)講述。
參數(shù)默認(rèn)值
給函數(shù)參數(shù)設(shè)一個默認(rèn)值恐怕是JavaScript里最常見的寫法了。長久以來我們大概都是這么做的,你可能對下面的代碼似曾相識:
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)然,如果以前這樣做過,你可能已經(jīng)知道這樣做有利有弊,比如在參數(shù)值和false
等價的時候:
foo( 0, 42 ); // 53 <-- 哦豁,不是42喔。
為什么會這樣?因為0
是一個假值,所以x || 11
會得到11
,而不會把0
傳過去。
為了避免這個小意外,有些人會用稍顯羅嗦的辦法來實現(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
會被當(dāng)成信號,表示“我沒有傳這個參數(shù)”。這么做沒有問題,除非哪天你真的需要把undefined
傳進(jìn)去。
如果是這樣,你可以測試這個參數(shù)是否真的被忽略了,通過檢查它是否出現(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
但是你怎么可能不傳點什么值標(biāo)明“我要跳過這個參數(shù)”(連undefined
都不傳),就跳過第一個參數(shù)x
?
foo(,5)
看起來不錯,但這是個語法錯誤。foo.apply(null,[,5])
看起來好像可以工作,但apply(..)
有個怪癖,這樣的參數(shù)會被當(dāng)成[undefined,5]
,也就是說也不會被跳過的。
如果你深挖下去,你會發(fā)現(xiàn)你只能省略末尾的參數(shù)(即右邊的),只需要比所需的參數(shù)少傳幾個就可以了,但你不能跳過參數(shù)中間或者前面的幾個。就是做不到。
JavaScript設(shè)計中有一個重要的原則就是undefined
通常意味著缺失。也就是說undefined
和缺失之間是沒區(qū)別的,至少在函數(shù)參數(shù)這里是這樣的。
注意:比較混亂的是,JS里也有很多不符合設(shè)計模式的地方,比如數(shù)組里的空位之類的。見本系列中的《類型和語法》一書。
鋪墊了這么多,我們現(xiàn)在來看一個新的ES6語法,它好看又實用,可以高效地幫我們給缺失的參數(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`不見了
foo( 5, null ); // 5 <-- `null`強制轉(zhuǎn)換成了`0`
foo( undefined, 6 ); // 17 <-- `undefined`不見了
foo( null, 6 ); // 6 <-- `null`強制轉(zhuǎn)換成了`0`
注意我們得到的輸出,看它們和之前做法的細(xì)微差別和相似之處。
比起慣用的x || 11
,函數(shù)聲明中的x == 11
和x !== undefined ? x : 11
的行為更相似,所以在把前ES6的代碼轉(zhuǎn)換成ES6的時候,你要格外小心這些有默認(rèn)值的語法。
注意: 其余/聚合參數(shù)(見《展開和其余》)不能設(shè)置默認(rèn)值。所以,當(dāng)function foo(...vals=[1,2,3]) {
看起來好像很不錯的樣子,但實際上這個語法并不成立。這種情況下你還是需要繼續(xù)手寫前面的邏輯來實現(xiàn)。
默認(rèn)值表達(dá)式
函數(shù)的默認(rè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
如你所見,這些默認(rèn)值表達(dá)式是延遲執(zhí)行的,這意味著它們只在被調(diào)用的時候執(zhí)行——也就是,只有當(dāng)函數(shù)參數(shù)被省略或者是undefined
的時候。
這是個很微妙的細(xì)節(jié),但是函數(shù)聲明里正式參數(shù)是在它們自己作用域里的(把它想象成一個用( .. )
包裹起來的作用域氣泡),而不是在函數(shù)體作用域里。這意味著在默認(rèn)值表達(dá)式里的對一個標(biāo)識符的引用會首先匹配正式的參數(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
會在正式的參數(shù)作用域中尋找w
,但是沒找到,它就會退而求其次來使用外部作用域中的w
。接下來,默認(rèn)值表達(dá)式x + 1
也會在正式的參數(shù)作用域中尋找x
,幸運的是這時候x
已經(jīng)初始化了,所以y
的賦值也沒有問題。
然而,z + 1
里的z
就沒那么好運了,它在那一刻還沒有初始化的變量,所以它也不會在外部作用域里費力找z
。
我們在let
聲明一章已經(jīng)提到過,ES6有TDZ(暫時性死區(qū)),會阻止我們訪問一個還沒初始化的變量。在這里,默認(rèn)值表達(dá)式z + 1
就會拋出一個TDZ錯誤ReferenceError
。
盡管對代碼清晰度來說并不是個好的主意,但默認(rèn)值表達(dá)式確實可以寫成一個inline的函數(shù)表達(dá)式調(diào)用——一般稱為立即執(zhí)行函數(shù)(IIFE):
function foo( x =
(function(v){ return v + 11; })( 31 )
) {
console.log( x );
}
foo(); // 42
這絕對不是默認(rèn)值表達(dá)式和立即執(zhí)行函數(shù)正常的相處方式,如果你發(fā)現(xiàn)你在朝這條路上走,那么請退一步重新審視一下自己為什么要這么做!
警告:如果立即執(zhí)行函數(shù)嘗試去訪問標(biāo)識符x
,并且還沒有聲明自己的x
,那么它也會得到一個TDZ錯誤,和之前提到的一樣。
在前面代碼里的默認(rèn)值表達(dá)式是一個立即執(zhí)行函數(shù),通過立即調(diào)用的(31)
執(zhí)行的。如果不小心遺漏了這部分,那么x
的默認(rèn)值將是函數(shù)引用本身,可能就像是一個回調(diào)函數(shù)。這樣么做可能還比較有用,例如:
function ajax(url, cb = function(){}) {
// ..
}
ajax( "http://some.url.1" );
在這種情況下,我們非常希望沒賦值的cb
是空操作,如果沒有任何其他的指令。這里的函數(shù)表達(dá)式只是一個引用,并不是函數(shù)調(diào)用本身(我們省略了末尾的()
),反而達(dá)到了我們的目的。
早期的JS里有個不太為人知但是確實有用的技巧:Function.prototype
本身就是一個空的無操作函數(shù)。所以上面的表達(dá)式可以寫成cb = Function.prototype
,這樣就可以省掉函數(shù)表達(dá)式了。
該系列文章翻譯自Kyle Simpson的《You don't know about Javascript》,本章原文在此。