感謝社區(qū)中各位的大力支持诵次,譯者再次奉上一點點福利:阿里云產(chǎn)品券账蓉,享受所有官網(wǎng)優(yōu)惠,并抽取幸運大獎:點擊這里領取
如果你曾經(jīng)或多或少地寫過JS逾一,那么你很可能對它的語法感到十分熟悉铸本。當然有一些奇怪之處,但是總體來講這是一種與其他語言有很多相似之處的遵堵,相當合理而且直接的語法箱玷。
然而,ES6增加了好幾種需要費些功夫才能習慣的新語法形式陌宿。在這一章中锡足,我們將遍歷它們來看看葫蘆里到底賣的什么藥。
提示: 在寫作本書時壳坪,這本書中所討論的特性中的一些已經(jīng)被各種瀏覽器(Firefox舶得,Chrome,等等)實現(xiàn)了爽蝴,但是有一些僅僅被實現(xiàn)了一部分沐批,而另一些根本就沒實現(xiàn)。如果直接嘗試這些例子霜瘪,你的體驗可能會夾雜著三種情況珠插。如果是這樣,就使用轉譯器嘗試吧颖对,這些特性中的大多數(shù)都被那些工具涵蓋了捻撑。ES6Fiddle(http://www.es6fiddle.net/)是一個了不起的嘗試ES6的游樂場,簡單易用,它是一個Babel轉譯器的在線REPL(http://babeljs.io/repl/)顾患。
塊兒作用域聲明
你可能知道在JavaScript中變量作用域的基本單位總是function
番捂。如果你需要創(chuàng)建一個作用域的塊兒,除了普通的函數(shù)聲明以外最流行的方法就是使用立即被調(diào)用的函數(shù)表達式(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中使用獨立的{ .. }
塊兒不是很常見灭翔,也不是慣用模式魏烫,但它總是合法的。而且那些來自擁有 塊兒作用域 的語言的開發(fā)者將很容易認出這種模式肝箱。
我相信使用一個專門的{ .. }
塊兒是創(chuàng)建塊兒作用域變量的最佳方法哄褒。但是,你應該總是將let
聲明放在塊兒的最頂端煌张。如果你有多于一個的聲明呐赡,我推薦只使用一個let
。
從文體上說唱矛,我甚至喜歡將let
放在與開放的{
的同一行中罚舱,以便更清楚地表示這個塊兒的目的僅僅是為了這些變量聲明作用域。
{ let a = 2, b, c;
// ..
}
它現(xiàn)在看起來很奇怪绎谦,而且不大可能與其他大多數(shù)ES6文獻中推薦的文法吻合管闷。但我的瘋狂是有原因的。
這是另一種實驗性的(不是標準化的)let
聲明形式窃肠,稱為let
塊兒包个,看起來就像這樣:
let (a = 2, b, c) {
// ..
}
我稱這種形式為 明確的 塊兒作用域,而與var
相似的let
聲明形式更像是 隱含的冤留,因為它在某種意義上劫持了它所處的{ .. }
碧囊。一般來說開發(fā)者們認為 明確的 機制要比 隱含的 機制更好一些,我主張這種情況就是這樣的情況之一纤怒。
如果你比較前面兩個形式的代碼段糯而,它們非常相似,而且我個人認為兩種形式都有資格在文體上稱為 明確的 塊兒作用域泊窘。不幸的是熄驼,兩者中最 明確的 let (..) { .. }
形式?jīng)]有被ES6所采用像寒。它可能會在后ES6時代被重新提起,但我想目前為止前者是我們的最佳選擇瓜贾。
為了增強對let ..
聲明的 隱含 性質(zhì)的理解诺祸,考慮一下這些用法:
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
語句內(nèi)部祭芦?哪些變量僅存在于for
循環(huán)內(nèi)部筷笨?
答案:if
語句包含塊兒作用域變量b
和c
,而for
循環(huán)包含塊兒作用域變量i
和j
龟劲。
你有任何遲疑嗎胃夏?i
沒有被加入外圍的if
語句的作用域讓你驚訝嗎?思維上的停頓和疑問 —— 我稱之為“思維稅” —— 不僅源自于let
機制對我們來說是新東西昌跌,還因為它是 隱含的构订。
還有一個災難是let c = ..
聲明出現(xiàn)在作用域中太過靠下的地方。傳統(tǒng)的被var
聲明的變量避矢,無論它們出現(xiàn)在何處,都會被附著在整個外圍的函數(shù)作用域中囊榜;與此不同的是审胸,let
聲明附著在塊兒作用域,而且在它們出現(xiàn)在塊兒中之前是不會被初始化的卸勺。
在一個let ..
聲明/初始化之前訪問一個用let
聲明的變量會導致一個錯誤砂沛,而對于var
聲明來說這個順序無關緊要(除了文體上的區(qū)別)。
考慮如下代碼:
{
console.log( a ); // undefined
console.log( b ); // ReferenceError!
var a;
let b;
}
警告: 這個由于過早訪問被let
聲明的引用而引起的ReferenceError
在技術上稱為一個 臨時死區(qū)(Temporal Dead Zone —— TDZ) 錯誤 —— 你在訪問一個已經(jīng)被聲明但還沒被初始化的變量曙求。這將不是我們唯一能夠見到TDZ錯誤的地方 —— 在ES6中它們會在幾種地方意外地發(fā)生碍庵。另外,注意“初始化”并不要求在你的代碼中明確地賦一個值悟狱,比如let b;
是完全合法的静浴。一個在聲明時沒有被賦值的變量被認為已經(jīng)被賦予了undefined
值,所以let b;
和let b = undefined;
是一樣的挤渐。無論是否明確賦值苹享,在let b
語句運行之前你都不能訪問b
。
最后一個坑:對于TDZ變量和未聲明的(或聲明的T÷椤)變量得问,typeof
的行為是不同的。例如:
{
// `a` 沒有被聲明
if (typeof a === "undefined") {
console.log( "cool" );
}
// `b` 被聲明了软免,但位于它的TDZ中
if (typeof b === "undefined") { // ReferenceError!
// ..
}
// ..
let b;
}
a
沒有被聲明宫纬,所以typeof
是檢查它是否存在的唯一安全的方法。但是typeof b
拋出了TDZ錯誤膏萧,因為在代碼下面很遠的地方偶然出現(xiàn)了一個let b
聲明漓骚。噢蝌衔。
現(xiàn)在你應當清楚為什么我堅持認為所有的let
聲明都應該位于它們作用域的頂部了。這完全避免了偶然過早訪問的錯誤认境。當你觀察一個塊兒胚委,或任何塊兒的開始部分時,它還更 明確 地指出這個塊兒中含有什么變量叉信。
你的塊兒(if
語句亩冬,while
循環(huán),等等)不一定要與作用域行為共享它們原有的行為硼身。
這種明確性要由你負責硅急,由你用毅力來維護,它將為你省去許多重構時的頭疼和后續(xù)的麻煩佳遂。
注意: 更多關于let
和塊兒作用域的信息营袜,參見本系列的 作用域與閉包 的第三章。
let
+ for
我偏好 明確 形式的let
聲明塊兒丑罪,但對此的唯一例外是出現(xiàn)在for
循環(huán)頭部的let
荚板。這里的原因看起來很微妙,但我相信它是更重要的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
。這意味著在循環(huán)迭代內(nèi)部創(chuàng)建的閉包都分別引用著那些在每次迭代中創(chuàng)建的變量煤搜,正如你期望的那樣免绿。
如果你嘗試在這段相同代碼的for
循環(huán)頭部使用var i
,那么你會得到5
而不是3
擦盾,因為在被引用的外部作用域中只有一個i
嘲驾,而不是為每次迭代的函數(shù)都有一個i
被引用。
你也可以稍稍繁冗地實現(xiàn)相同的東西:
var funcs = [];
for (var i = 0; i < 5; i++) {
let j = i;
funcs.push( function(){
console.log( j );
} );
}
funcs[3](); // 3
在這里迹卢,我們強制地為每次迭代都創(chuàng)建一個新的j
辽故,然后閉包以相同的方式工作。我喜歡前一種形式婶希;那種額外的特殊能力正是我支持for(let .. ) ..
形式的原因榕暇。可能有人會爭論說它有點兒 隱晦喻杈,但是對我的口味來說彤枢,它足夠 明確 了,也足夠有用筒饰。
let
在for..in
和for..of
(參見“for..of
循環(huán)”)循環(huán)中也以形同的方式工作缴啡。
const
聲明
還有另一種需要考慮的塊兒作用域聲明:const
,它創(chuàng)建 常量瓷们。
到底什么是一個常量业栅?它是一個在初始值被設定后就成為只讀的變量秒咐。考慮如下代碼:
{
const a = 2;
console.log( a ); // 2
a = 3; // TypeError!
}
變量持有的值一旦在聲明時被設定就不允許你改變了碘裕。一個const
聲明必須擁有一個明確的初始化携取。如果想要一個持有undefined
值的 常量,你必須聲明const a = undefined
來得到它帮孔。
常量不是一個作用于值本身的制約雷滋,而是作用于變量對這個值的賦值。換句話說文兢,值不會因為const
而凍結或不可變晤斩,只是它的賦值被凍結了。如果這個值是一個復雜值姆坚,比如對象或數(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ù)組作為常量賦值意味著這個值在常量的詞法作用域消失以前是不能夠被垃圾回收的,因為指向這個值的引用是永遠不能解除的击喂。這可能是你期望的幢妄,但如果不是你就要小心!
實質(zhì)上茫负,const
聲明強制實行了我們許多年來在代碼中用文體來表明的東西:我們聲明一個名稱全由大寫字母組成的變量并賦予它某些字面值,我們小心照看它以使它永不改變乎赴。var
賦值沒有強制性忍法,但是現(xiàn)在const
賦值上有了,它可以幫你發(fā)現(xiàn)不經(jīng)意的改變榕吼。
const
可以 被用于for
饿序,for..in
,和for..of
循環(huán)(參見“for..of
循環(huán)”)的變量聲明羹蚣。然而原探,如果有任何重新賦值的企圖,一個錯誤就會被拋出顽素,例如在for
循環(huán)中常見的i++
子句咽弦。
const
用還是不用
有些流傳的猜測認為在特定的場景下,與let
或var
相比一個const
可能會被JS引擎進行更多的優(yōu)化胁出。理論上型型,引擎可以更容易地知道變量的值/類型將永遠不會改變,所以它可以免除一些可能的追蹤工作全蝶。
無論const
在這方面是否真的有幫助闹蒜,還是這僅僅是我們的幻想和直覺寺枉,你要做的更重要的決定是你是否打算使用常量的行為。記妆谅洹:源代碼扮演的一個最重要的角色是為了明確地交流你的意圖是什么姥闪,不僅是與你自己,而且還是與未來的你和其他的代碼協(xié)作者砌烁。
一些開發(fā)者喜歡在一開始將每個變量都聲明為一個const
筐喳,然后當它的值在代碼中有必要發(fā)生變化的時候將聲明放松至一個let
。這是一個有趣的角度往弓,但是不清楚這是否真正能夠改善代碼的可讀性或可推理性疏唾。
就像許多人認為的那樣,它不是一種真正的 保護函似,因為任何后來的想要改變一個const
值的開發(fā)者都可以盲目地將聲明從const
改為let
槐脏。它至多是防止意外的改變。但是同樣地撇寞,除了我們的直覺和感覺以外顿天,似乎沒有客觀和明確的標準可以衡量什么構成了“意外”或預防措施。這與類型強制上的思維模式類似蔑担。
我的建議:為了避免潛在的令人糊涂的代碼牌废,僅將const
用于那些你有意地并且明顯地標識為不會改變的變量。換言之啤握,不要為了代碼行為而 依靠 const
鸟缕,而是在為了意圖可以被清楚地表明時,將它作為一個表明意圖的工具排抬。
塊兒作用域的函數(shù)
從ES6開始懂从,發(fā)生在塊兒內(nèi)部的函數(shù)聲明現(xiàn)在被明確規(guī)定屬于那個塊兒的作用域。在ES6之前蹲蒲,語言規(guī)范沒有要求這一點番甩,但是許多實現(xiàn)不管怎樣都是這么做的。所以現(xiàn)在語言規(guī)范和現(xiàn)實吻合了届搁。
考慮如下代碼:
{
foo(); // 好用缘薛!
function foo() {
// ..
}
}
foo(); // ReferenceError
函數(shù)foo()
是在{ .. }
塊兒內(nèi)部被聲明的,由于ES6的原因它是屬于那里的塊兒作用域的卡睦。所以在那個塊兒的外部是不可用的隅津。但是還要注意它在塊兒里面被“提升”了异赫,這與早先提到的遭受TDZ錯誤陷阱的let
聲明是相反的。
如果你以前曾經(jīng)寫過這樣的代碼,并依賴于老舊的非塊兒作用域行為的話竣付,那么函數(shù)聲明的塊兒作用域可能是一個問題:
if (something) {
function foo() {
console.log( "1" );
}
}
else {
function foo() {
console.log( "2" );
}
}
foo(); // ??
在前ES6環(huán)境下田巴,無論something
的值是什么foo()
都將會打印"2"
,因為兩個函數(shù)聲明被提升到了塊兒的頂端,而且總是第二個有效补胚。
在ES6中,最后一行將拋出一個ReferenceError
追迟。
擴散/剩余
ES6引入了一個新的...
操作符溶其,根據(jù)你在何處以及如何使用它,它一般被稱作 擴散(spread) 或 剩余(rest) 操作符敦间。讓我們看一看:
function foo(x,y,z) {
console.log( x, y, z );
}
foo( ...[1,2,3] ); // 1 2 3
當...
在一個數(shù)組(實際上瓶逃,是我們將在第三章中講解的任何的 可迭代 對象)前面被使用時,它就將數(shù)組“擴散”為它的個別的值廓块。
通常你將會在前面所展示的那樣的代碼段中看到這種用法厢绝,它將一個數(shù)組擴散為函數(shù)調(diào)用的一組參數(shù)。在這種用法中带猴,...
扮演了apply(..)
方法的簡約語法替代品昔汉,在前ES6中我們經(jīng)常這樣使用apply(..)
:
foo.apply( null, [1,2,3] ); // 1 2 3
但...
也可以在其他上下文環(huán)境中被用于擴散/展開一個值,比如在另一個數(shù)組聲明內(nèi)部:
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
實質(zhì)上是在說:“將 剩余的 參數(shù)值(如果有的話)收集到一個稱為z
的數(shù)組中沪停∶罕妫” 因為x
被賦值為1
,而y
被賦值為2
木张,所以剩余的參數(shù)值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
經(jīng)常因為你向其中收集參數(shù)的剩余部分而被稱為“剩余參數(shù)”且轨。我喜歡使用“收集”這個詞,因為它描述了它做什么而不是它包含什么虚婿。
這種用法最棒的地方是旋奢,它為被廢棄了很久的arguments
數(shù)組 —— 實際上它不是一個真正的數(shù)組,而是一個類數(shù)組對象 —— 提供了一種非常穩(wěn)健的替代方案然痊。因為args
(無論你叫它什么 —— 許多人喜歡叫它r
或者rest
)是一個真正的數(shù)組至朗,我們可以擺脫許多愚蠢的前ES6技巧,我們曾經(jīng)通過這些技巧盡全力去使arguments
變成我們可以視之為數(shù)組的東西剧浸。
考慮如下代碼:
// 使用新的ES6方式
function foo(...args) {
// `args`已經(jīng)是一個真正的數(shù)組了
// 丟棄`args`中的第一個元素
args.shift();
// 將`args`的所有內(nèi)容作為參數(shù)值傳給`console.log(..)`
console.log( ...args );
}
// 使用老舊的前ES6方式
function bar() {
// 將`arguments`轉換為一個真正的數(shù)組
var args = Array.prototype.slice.call( arguments );
// 在末尾添加一些元素
args.push( 4, 5 );
// 過濾掉所有奇數(shù)
args = args.filter( function(v){
return v % 2 == 0;
} );
// 將`args`的所有內(nèi)容作為參數(shù)值傳給`foo(..)`
foo.apply( null, args );
}
bar( 0, 1, 2, 3 ); // 2 4
在函數(shù)foo(..)
聲明中的...args
收集參數(shù)值锹引,而在console.log(..)
調(diào)用中的...args
將它們擴散開矗钟。這個例子很好地展示了...
操作符平行但相反的用途。
除了在函數(shù)聲明中...
的用法以外嫌变,還有另一種...
被用于收集值的情況吨艇,我們將在本章稍后的“太多,太少腾啥,正合適”一節(jié)中檢視它东涡。
默認參數(shù)值
也許在JavaScript中最常見的慣用法之一就是為函數(shù)參數(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
當然倘待,如果你曾經(jīng)用過這種模式疮跑,你就會知道它既有用又有點兒危險,例如如果你需要能夠為其中一個參數(shù)傳入一個可能被認為是falsy的值凸舵∽婺铮考慮下面的代碼:
foo( 0, 42 ); // 53 <-- 噢,不是42
為什么贞间?因為0
是falsy贿条,因此x || 11
的結果為11
,而不是直接被傳入的0
增热。
為了填這個坑整以,一些人會像這樣更加啰嗦地編寫檢查:
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
,它就工作的很好吭从。
在那樣的情況下朝蜘,你可以通過測試參數(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
但是在沒有能力傳入意味著“我省略了這個參數(shù)值”的任何種類的值(連undefined
也不行)的情況下谱醇,你如何才能省略第一個參數(shù)值x
呢?
foo(,5)
很誘人步做,但它不是合法的語法副渴。foo.apply(null,[,5])
看起來應該可以實現(xiàn)這個技巧,但是apply(..)
的奇怪之處意味著這組參數(shù)值將被視為[undefined,5]
全度,顯然它沒有被省略煮剧。
如果你深入調(diào)查下去,你將發(fā)現(xiàn)你只能通過簡單地傳入比“期望的”參數(shù)值個數(shù)少的參數(shù)值來省略末尾的參數(shù)值,但是你不能省略在參數(shù)值列表中間或者開頭的參數(shù)值勉盅。這就是不可能佑颇。
這里有一個施用于JavaScript設計的重要原則需要記住:undefined
意味著 缺失菇篡。也就是漩符,在undefined
和 缺失 之間沒有區(qū)別,至少是就函數(shù)參數(shù)值而言驱还。
注意: 容易令人糊涂的是嗜暴,JS中有其他的地方不適用這種特殊的設計原則,比如帶有空值槽的數(shù)組议蟆。更多信息參見本系列的 類型與文法闷沥。
帶著所有這些認識,現(xiàn)在我們可以檢視在ES6中新增的一種有用的好語法咐容,來簡化對丟失的參數(shù)值進行默認值的賦值舆逃。
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`
注意這些結果,和它們?nèi)绾伟凳玖伺c前面的方式的微妙區(qū)別和相似之處戳粒。
與常見得多的x || 11
慣用法相比路狮,在一個函數(shù)聲明中的x = 11
更像x !== undefined ? x : 11
,所以在將你的前ES6代碼轉換為這種ES6默認參數(shù)值語法時要多加小心蔚约。
注意: 一個剩余/收集參數(shù)(參見“擴散/剩余”)不能擁有默認值奄妨。所以,雖然function foo(...vals=[1,2,3]) {
看起來是一種迷人的能力苹祟,但它不是合法的語法砸抛。有必要的話你需要繼續(xù)手動實施那種邏輯。
默認值表達式
函數(shù)默認值可以比像31
這樣的簡單值復雜得多树枫;它們可以是任何合法的表達式直焙,甚至是函數(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
如你所見,默認值表達式是被懶惰地求值的砂轻,這意味著他們僅在被需要時運行 —— 也就是奔誓,當一個參數(shù)的參數(shù)值被省略或者為undefined
。
這是一個微妙的細節(jié)搔涝,但是在一個函數(shù)聲明中的正式參數(shù)是在它們自己的作用域中的(將它想象為一個僅僅圍繞在函數(shù)聲明的(..)
外面的一個作用域氣泡)厨喂,不是在函數(shù)體的作用域中。這意味著在一個默認值表達式中的標識符引用會在首先在正式參數(shù)的作用域中查找標識符体谒,然后再查找一個外部作用域。更多信息參見本系列的 作用域與閉包臼婆。
考慮如下代碼:
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
在正式參數(shù)作用域中查找w
抒痒,但沒有找到,所以外部作用域的w
被使用了颁褂。接下來故响,在默認值表達式x + 1
中的x
在正式參數(shù)的作用域中找到了x
傀广,而且走運的是x
已經(jīng)被初始化了,所以對y
的賦值工作的很好彩届。
然而伪冰,z + 1
中的z
找到了一個在那個時刻還沒有被初始化的參數(shù)變量z
,所以它絕不會試著在外部作用域中尋找z
樟蠕。
正如我們在本章早先的“let
聲明”一節(jié)中提到過的那樣贮聂,ES6擁有一個TDZ,它會防止一個變量在它還沒有被初始化的狀態(tài)下被訪問寨辩。因此吓懈,z + 1
默認值表達式拋出一個TDZReferenceError
錯誤。
雖然對于代碼的清晰度來說不見得是一個好主意靡狞,一個默認值表達式甚至可以是一個內(nèi)聯(lián)的函數(shù)表達式調(diào)用 —— 通常被稱為一個立即被調(diào)用的函數(shù)表達式(IIFE):
function foo( x =
(function(v){ return v + 11; })( 31 )
) {
console.log( x );
}
foo(); // 42
一個IIFE(或者任何其他被執(zhí)行的內(nèi)聯(lián)函數(shù)表達式)作為默認值表示來說很合適是非常少見的耻警。如果你發(fā)現(xiàn)自己試圖這么做,那么就退一步再考慮一下甸怕!
警告: 如果一個IIFE試圖訪問標識符x
甘穿,而且還沒有聲明自己的x
,那么這也將是一個TDZ錯誤梢杭,就像我們剛才討論的一樣温兼。
前一個代碼段的默認值表達式是一個IIFE,這是因為它是通過(31)
在內(nèi)聯(lián)時立即被執(zhí)行式曲。如果我們?nèi)サ暨@一部分妨托,賦予x
的默認值將會僅僅是一個函數(shù)的引用,也許像一個默認的回調(diào)吝羞±忌耍可能有一些情況這種模式將十分有用,比如:
function ajax(url, cb = function(){}) {
// ..
}
ajax( "http://some.url.1" );
這種情況下钧排,我們實質(zhì)上想在沒有其他值被指定時敦腔,讓默認的cb
是一個沒有操作的空函數(shù)。這個函數(shù)表達式只是一個函數(shù)引用恨溜,不是一個調(diào)用它自己(在它末尾沒有調(diào)用的()
)以達成自己目的的函數(shù)符衔。
從JS的早些年開始,就有一個少為人知但是十分有用的奇怪之處可供我們使用:Function.prototype
本身就是一個沒有操作的空函數(shù)糟袁。這樣判族,這個聲明可以是cb = Function.prototype
而省去內(nèi)聯(lián)函數(shù)表達式的創(chuàng)建。
解構
ES6引入了一個稱為 解構 的新語法特性项戴,如果你將它考慮為 結構化賦值 那么它令人困惑的程度可能會小一些形帮。為了理解它的含義,考慮如下代碼:
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
如你所見,我們創(chuàng)建了一個手動賦值:從foo()
返回的數(shù)組中的值到個別的變量a
辩撑,b
界斜,和c
,而且這么做我們就(不幸地)需要tmp
變量合冀。
相似地各薇,我們也可以用對象這么做:
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
也一樣君躺。
從一個數(shù)組中取得索引的值峭判,或從一個對象中取得屬性并手動賦值可以被認為是 結構化賦值。ES6為 解構 增加了一種專門的語法晰洒,具體地稱為 數(shù)組解構 和 對象結構朝抖。這種語法消滅了前一個代碼段中對變量tmp
的需要,使它們更加干凈谍珊≈涡考慮如下代碼:
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]
這樣的東西出現(xiàn)在一個=
賦值的右手邊的語法,即作為要被賦予的值砌滞。
解構對稱地翻轉了這個模式侮邀,所以在=
賦值左手邊的[a,b,c]
被看作是為了將右手邊的數(shù)組拆解為分離的變量賦值的某種“模式”。
類似地贝润,{ x: x, y: y, z: z }
指明了一種“模式”把來自于bar()
的對象拆解為分離的變量賦值绊茧。
對象屬性賦值模式
讓我們深入前一個代碼段中的{ x: x, .. }
語法。如果屬性名與你想要聲明的變量名一致打掘,你實際上可以縮寫這個語法:
var { x, y, z } = bar();
console.log( x, y, z ); // 4 5 6
很酷华畏,對吧?
但{ x, .. }
是省略了x:
部分還是省略了: x
部分尊蚁?當我們使用這種縮寫語法時亡笑,我們實際上省略了x:
部分。這看起來可能不是一個重要的細節(jié)横朋,但是一會兒你就會了解它的重要性仑乌。
如果你能寫縮寫形式,那為什么你還要寫出更長的形式呢琴锭?因為更長的形式事實上允許你將一個屬性賦值給一個不同的變量名稱晰甚,這有時很有用:
var { x: bam, y: baz, z: bap } = bar();
console.log( bam, baz, bap ); // 4 5 6
console.log( x, y, z ); // ReferenceError
關于這種對象結構形式有一個微妙但超級重要的怪異之處需要理解。為了展示為什么它可能是一個你需要注意的坑决帖,讓我們考慮一下普通對象字面量的“模式”是如何被指定的:
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
是被賦值給它的源值地回。換句話說扁远,它的語義模式是目標: 源
腺阳,或者更明顯地,屬性別名: 值
穿香。我們能直觀地明白這一點,因為它和=
賦值是一樣的绎速,而它的模式就是目標 = 源
皮获。
然而,當你使用對象解構賦值時 —— 也就是纹冤,將看起來像是對象字面量的{ .. }
語法放在=
操作符的左手邊 —— 你反轉了這個目標: 源
的模式洒宝。
回想一下:
var { x: bam, y: baz, z: bap } = bar();
這里面對稱的模式是源: 目標
(或者值: 屬性別名
)。x: bam
意味著屬性x
是源值而ban
是被賦值的目標變量萌京。換句話說雁歌,對象字面量是target <-- source
,而對象解構賦值是source --> target
知残】肯梗看到它是如何反轉的了嗎?
有另外一種考慮這種語法的方式求妹,可能有助于緩和這種困惑乏盐。考慮如下代碼:
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
模式。唉梨树,我已經(jīng)被迫訓練自己的大腦去習慣這種反轉了坑夯,就像一些讀者也不得不去做的那樣。
不僅是聲明
至此抡四,我們一直將解構賦值與var
聲明(當然柜蜈,它們也可以使用let
和const
)一起使用仗谆,但是解構是一種一般意義上的賦值操作,不僅是一種聲明淑履。
考慮如下代碼:
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
變量可以是已經(jīng)被定義好的隶垮,然后解構僅僅負責賦值,正如我們已經(jīng)看到的那樣秘噪。
注意: 特別對于對象解構形式來說狸吞,當我們省略了var
/let
/const
聲明符時,就必須將整個賦值表達式包含在()
中指煎,因為如果不這樣做的話左手邊作為語句第一個元素的{ .. }
將被視為一個語句塊兒而不是一個對象蹋偏。
事實上,變量表達式(a
至壤,y
威始,等等)不必是一個變量標識符。任何合法的賦值表達式都是允許的像街。例如:
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
你甚至可以在解構中使用計算型屬性名黎棠。考慮如下代碼:
var which = "x",
o = {};
( { [which]: o[which] } = bar() );
console.log( o.x ); // 4
[which]:
的部分是計算型屬性名镰绎,它的結果是x
—— 將從當前的對象中拆解出來作為賦值的源頭的屬性葫掉。o[which]
的部分只是一個普通的對象鍵引用,作為賦值的目標來說它與o.x
是等價的跟狱。
你可以使用普通的賦值來創(chuàng)建對象映射/變形俭厚,例如:
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
或者你可以將對象映射進一個數(shù)組,例如:
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]
或者從另一個方向:
var a1 = [ 1, 2, 3 ],
o2 = {};
[ o2.a, o2.b, o2.c ] = a1;
console.log( o2.a, o2.b, o2.c ); // 1 2 3
或者你可以將一個數(shù)組重排到另一個數(shù)組中:
var a1 = [ 1, 2, 3 ],
a2 = [];
[ a2[2], a2[0], a2[1] ] = a1;
console.log( a2 ); // [2,3,1]
你甚至可以不使用臨時變量來解決傳統(tǒng)的“交換兩個變量”的問題:
var x = 10, y = 20;
[ y, x ] = [ x, y ];
console.log( x, y ); // 20 10
警告: 小心:你不應該將聲明和賦值混在一起驶臊,除非你想要所有的賦值表達式 也 被視為聲明挪挤。否則,你會得到一個語法錯誤关翎。這就是為什么在剛才的例子中我必須將var a2 = []
與[ a2[0], .. ] = ..
解構賦值分開做扛门。嘗試var [ a2[0], .. ] = ..
沒有任何意義,因為a2[0]
不是一個合法的聲明標識符纵寝;很顯然它也不能隱含地創(chuàng)建一個var a2 = []
聲明來使用论寨。
重復賦值
對象解構形式允許源屬性(持有任意值的類型)被羅列多次。例如:
var { a: X, a: Y } = { a: 1 };
X; // 1
Y; // 1
這意味著你既可以解構一個子對象/數(shù)組屬性爽茴,也可以捕獲這個子對象/數(shù)組的值本身葬凳。考慮如下代碼:
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
關于解構有一句話要提醒:像我們到目前為止的討論中做的那樣室奏,將所有的解構賦值都羅列在單獨一行中的方式可能很誘人火焰。然而,一個好得多的主意是使用恰當?shù)目s進將解構賦值的模式分散在多行中 —— 和你在JSON或對象字面量中做的事非常相似 —— 為了可讀性胧沫。
// 很難讀懂:
var { a: { b: [ c, d ], e: { f } }, g } = obj;
// 好一些:
var {
a: {
b: [ c, d ],
e: { f }
},
g
} = obj;
記撞颉:解構的目的不僅是為了少打些字占业,更多是為了聲明可讀性
解構賦值表達式
帶有對象或數(shù)組解構的賦值表達式的完成值是右手邊完整的對象/數(shù)組值〈渴辏考慮如下代碼:
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
的值。數(shù)組解構也是一樣:
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
通過將這個對象/數(shù)組作為完成值傳遞下去佑附,你可將解構賦值表達式鏈接在一起:
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
太多,太少仗考,正合適
對于數(shù)組解構賦值和對象解構賦值兩者來說音同,你不必分配所有出現(xiàn)的值。例如:
var [,b] = foo();
var { x, z } = bar();
console.log( b, x, z ); // 2 4 6
從foo()
返回的值1
和3
被丟棄了秃嗜,從bar()
返回的值5
也是权均。
相似地,如果你試著分配比你正在解構/拆解的值要多的值時锅锨,它們會如你所想的那樣安靜地退回到undefined
:
var [,,c,d] = foo();
var { w, z } = bar();
console.log( c, z ); // 3 6
console.log( d, w ); // undefined undefined
這種行為平行地遵循早先提到的“undefined
意味著缺失”原則叽赊。
我們在本章早先檢視了...
操作符,并看到了它有時可以用于將一個數(shù)組值擴散為它的分離值必搞,而有時它可以被用于相反的操作:將一組值收集進一個數(shù)組必指。
除了在函數(shù)聲明中的收集/剩余用法以外,...
可以在解構賦值中實施相同的行為恕洲。為了展示這一點塔橡,讓我們回想一下本章早先的一個代碼段:
var a = [2,3,4];
var b = [ 1, ...a, 5 ];
console.log( b ); // [1,2,3,4,5]
我們在這里看到因為...a
出現(xiàn)在數(shù)組[ .. ]
中值的位置,所以它將a
擴散開霜第。如果...a
出現(xiàn)一個數(shù)組解構的位置葛家,它會實施收集行為:
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
的數(shù)組中癞谒。
注意: 我們已經(jīng)看到...
是如何與數(shù)組一起工作的,但是對象呢刃榨?那不是一個ES6特性弹砚,但是參看第八章中關于一種可能的“ES6之后”的特性的討論,它可以讓...
擴散或者收集對象枢希。
默認值賦值
兩種形式的解構都可以為賦值提供默認值選項迅栅,它使用和早先討論過的默認函數(shù)參數(shù)值相似的=
語法。
考慮如下代碼:
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
你可以將默認值賦值與前面講過的賦值表達式語法組合在一起晴玖。例如:
var { x, y, z, w: WW = 20 } = bar();
console.log( x, y, z, WW ); // 4 5 6 20
如果你在一個解構中使用一個對象或者數(shù)組作為默認值读存,那么要小心不要把自己(或者讀你的代碼的其他開發(fā)者)搞糊涂了为流。你可能會創(chuàng)建一些非常難理解的代碼:
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
最終是什么值嗎让簿?花點兒時間好好考慮一下敬察,我能想象你的樣子。我會終結這個懸念:
console.log( x.y, y.y, z.y ); // 300 100 42
這里的要點是:解構很棒也可以很有用尔当,但是如果使用得不明智莲祸,它也是一把可以傷人(某人的大腦)的利劍。
嵌套解構
如果你正在解構的值擁有嵌套的對象或數(shù)組椭迎,你也可以解構這些嵌套的值:
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
嵌套的解構可以是一種將對象名稱空間扁平化的簡單方法锐帜。例如:
var App = {
model: {
User: function(){ .. }
}
};
// 取代:
// var User = App.model.User;
var { model: { User } } = App;
參數(shù)解構
你能在下面的代碼段中發(fā)現(xiàn)賦值嗎?
function foo(x) {
console.log( x );
}
foo( 42 );
其中的賦值有點兒被隱藏的感覺:當foo(42)
被執(zhí)行時42
(參數(shù)值)被賦值給x
(參數(shù))畜号。如果參數(shù)/參數(shù)值對是一種賦值缴阎,那么按常理說它是一個可以被解構的賦值,對吧简软?當然蛮拔!
考慮參數(shù)的數(shù)組解構:
function foo( [ x, y ] ) {
console.log( x, y );
}
foo( [ 1, 2 ] ); // 1 2
foo( [ 1 ] ); // 1 undefined
foo( [] ); // undefined undefined
參數(shù)也可以進行對象解構:
function foo( { x, y } ) {
console.log( x, y );
}
foo( { y: 1, x: 2 } ); // 2 1
foo( { y: 42 } ); // undefined 42
foo( {} ); // undefined undefined
這種技術是命名參數(shù)值(一個長期以來被渴求的JS特性!)的一種近似解法:對象上的屬性映射到被解構的同名參數(shù)上痹升。這也意味著我們免費地(在任何位置)得到了可選參數(shù)建炫,如你所見,省去“參數(shù)”x
可以如我們期望的那樣工作疼蛾。
當然肛跌,先前討論過的所有解構的種類對于參數(shù)解構來說都是可用的,包括嵌套解構察郁,默認值惋砂,和其他。解構也可以和其他ES6函數(shù)參數(shù)功能很好地混合在一起绳锅,比如默認參數(shù)值和剩余/收集參數(shù)西饵。
考慮這些快速的示例(當然這沒有窮盡所有可能的種類):
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 }) { .. }
為了展示一下,讓我們從這個代碼段中取一個例子來檢視:
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]
這里使用了兩個...
操作符鳞芙,他們都是將值收集到數(shù)組中(z
和w
)眷柔,雖然...z
是從第一個數(shù)組參數(shù)值的剩余值中收集,而...w
是從第一個之后的剩余主參數(shù)值中收集的原朝。
解構默認值 + 參數(shù)默認值
有一個微妙的地方你應當注意要特別小心 —— 解構默認值與函數(shù)參數(shù)默認值的行為之間的不同驯嘱。例如:
function f6({ x = 10 } = {}, { y } = { y: 10 }) {
console.log( x, y );
}
f6(); // 10 10
首先,看起來我們用兩種不同的方法為參數(shù)x
和y
都聲明了默認值10
喳坠。然而鞠评,這兩種不同的方式會在特定的情況下表現(xiàn)出不同的行為,而且這種區(qū)別極其微妙壕鹉。
考慮如下代碼:
f6( {}, {} ); // 10 undefined
等等剃幌,為什么會這樣聋涨?十分清楚,如果在第一個參數(shù)值的對象中沒有一個同名屬性被傳遞负乡,那么命名參數(shù)x
將默認為10
牍白。
但y
是undefined
是怎么回事兒?值{ y: 10 }
是一個作為函數(shù)參數(shù)默認值的對象抖棘,不是結構默認值茂腥。因此,它僅在第二個參數(shù)根本沒有被傳遞切省,或者undefined
被傳遞時生效最岗,
在前面的代碼段中,我們傳遞了第二個參數(shù)({}
)朝捆,所以默認值{ y: 10 }
不被使用般渡,而解構{ y }
會針對被傳入的空對象值{}
發(fā)生。
現(xiàn)在右蹦,將{ y } = { y: 10 }
與{ x = 10 } = {}
比較一下。
對于x
的使用形式來說歼捐,如果第一個函數(shù)參數(shù)值被省略或者是undefined
何陆,會默認地使用空對象{}
。然后豹储,不管在第一個參數(shù)值的位置上是什么值 —— 要么是默認的{}
贷盲,要么是你傳入的 —— 都會被{ x = 10 }
解構,它會檢查屬性x
是否被找到剥扣,如果沒有找到(或者是undefined
)巩剖,默認值10
會被設置到命名參數(shù)x
上。
深呼吸钠怯〖涯В回過頭去把最后幾段多讀幾遍。讓我們用代碼復習一下:
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
一般來說晦炊,與參數(shù)y
的默認行為比起來鞠鲜,參數(shù)x
的默認行為可能看起來更可取也更合理。因此断国,理解{ x = 10 } = {}
形式與{ y } = { y: 10 }
形式為何與如何不同是很重要的贤姆。
如果這仍然有點兒模糊,回頭再把它讀一遍稳衬,并親自把它玩弄一番霞捡。未來的你將會感謝你花了時間把這種非常微妙的,晦澀的細節(jié)的坑搞明白薄疚。
嵌套默認值:解構與重構
雖然一開始可能很難掌握碧信,但是為一個嵌套的對象的屬性設置默認值產(chǎn)生了一種有趣的慣用法:將對象解構與一種我成為 重構 的東西一起使用赊琳。
考慮在一個嵌套的對象結構中的一組默認值,就像下面這樣:
// 摘自:http://es-discourse.com/t/partial-default-arguments/120/7
var defaults = {
options: {
remove: true,
enable: false,
instance: {}
},
log: {
warn: true,
error: true
}
};
現(xiàn)在音婶,我們假定你有一個稱為config
的對象慨畸,它有一些這其中的值,但也許不全有衣式,而且你想要將所有的默認值設置到這個對象的缺失點上寸士,但不覆蓋已經(jīng)存在的特定設置:
var config = {
options: {
remove: false,
instance: null
}
};
你當然可以手動這樣做,就像你可能曾經(jīng)做過的那樣:
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
中克隆的屬性覆蓋它住册,像這樣:
config = Object.assign( {}, defaults, config );
這看起來好多了婶博,是吧?但是這里有一個重大問題荧飞!Object.assign(..)
是淺拷貝凡人,這意味著當它拷貝defaults.options
時,它僅僅拷貝這個對象的引用叹阔,而不是深度克隆這個對象的屬性到一個config.options
對象挠轴。Object.assign(..)
需要在你的對象樹的每一層中實施才能得到你期望的深度克隆。
注意: 許多JS工具庫/框架都為對象的深度克隆提供它們自己的選項耳幢,但是那些方式和它們的坑超出了我們在這里的討論范圍岸晦。
那么讓我們檢視一下ES6的帶有默認值的對象解構能否幫到我們:
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
賦值引用讥邻。
但還是太多了。讓我們看看能否做得更好院峡。
下面的技巧在你知道你正在解構的所有屬性的名稱都是唯一的情況下工作得最好兴使。但即使不是這樣的情況你也仍然可以使用它,只是沒有那么好 —— 你將不得不分階段解構照激,或者創(chuàng)建獨一無二的本地變量作為臨時的別名发魄。
如果我們將所有的屬性完全解構為頂層變量,那么我們就可以立即重構來重組原本的嵌套對象解構。
但是所有那些游蕩在外的臨時變量將會污染作用域励幼。所以汰寓,讓我們通過一個普通的{ }
包圍塊兒來使用塊兒作用域(參見本章早先的“塊兒作用域聲明”)。
// 將`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
聲明來達到圈占作用域的目的。你的解構賦值/默認值將位于參數(shù)列表中嵌削,而你的重構將位于函數(shù)體的return
語句中毛好。
在重構部分的{ warn, error }
語法可能是你初次見到;它稱為“簡約屬性”苛秕,我們將在下一節(jié)講解它肌访!