對(duì)于前面提出的問題,最常見的答案是JavaScript具有基于函數(shù)的作用域娱挨,意味著每聲明一個(gè)函數(shù)都會(huì)為其自身創(chuàng)建一個(gè)氣泡余指,而其他結(jié)構(gòu)都不會(huì)創(chuàng)建作用域氣泡。但事實(shí)上這并不完全正確跷坝,下面我們來看一下酵镜。大家還可以關(guān)注我的微信公眾號(hào),蝸牛全棧柴钻。
首先需要研究一下函數(shù)作用域機(jī)器背后的一些內(nèi)容淮韭。
考慮下面的代碼:
function foo(a){
var b = 2;
function bar(){
}
var c = 3;
}
在這個(gè)代碼片段中,foo的作用域氣泡中包含了標(biāo)識(shí)符a贴届、b靠粪、c和bar【其中a在函數(shù)foo的參數(shù)內(nèi)】蜡吧。無論標(biāo)識(shí)符聲明出現(xiàn)在作用域中的何處,這個(gè)標(biāo)識(shí)符所代表的變量或函數(shù)都將附屬于所處作用域的氣泡占键。我們將在后續(xù)的文章討論具體的原理昔善。
Bar擁有自己的作用域氣泡。全局作用域也有自己的作用域氣泡捞慌,它只包含了一個(gè)標(biāo)識(shí)符:foo耀鸦。
由于標(biāo)識(shí)符a、b啸澡、c和bar都附屬于foo的作用域氣泡袖订,因此無法從foo的外部對(duì)他們進(jìn)行訪問。也就是說嗅虏,這些標(biāo)識(shí)符全都無法從全局作用域中進(jìn)行訪問洛姑,因此下面的代碼會(huì)導(dǎo)致ReferenceError錯(cuò)誤:
bar(); // 失敗
console.log(a, b, c); // 三個(gè)全都失敗
【調(diào)用bar的時(shí)候,因?yàn)樵谌肿饔糜蛑幸矝]有找到皮服,所以出現(xiàn)的錯(cuò)誤是ReferenceError,楞艾,而不是TypeError】
但是,這些標(biāo)識(shí)符(a龄广、b硫眯、c、foo和bar)在foo內(nèi)部都是可以被訪問的择同,同樣在bar內(nèi)部也可以被訪問(假設(shè)bar內(nèi)部沒有同名的標(biāo)識(shí)符聲明)【如果有同名的標(biāo)識(shí)符聲明两入,會(huì)出現(xiàn)之前說的遮蔽效應(yīng)】
函數(shù)作用域的含義是指,屬于這個(gè)函數(shù)的全部變量都可以在整個(gè)函數(shù)的范圍內(nèi)使用及復(fù)用(實(shí)際上在嵌套的作用域中也可以使用)敲才。這種設(shè)計(jì)方案是非常有用的裹纳,能充分利用JavaScript變量可以根據(jù)需要改變值類型的“動(dòng)態(tài)”特性。
但與此同時(shí)紧武,如果不細(xì)心處理那些可以在整個(gè)作用域范圍內(nèi)被訪問的變量剃氧,可能會(huì)帶來意想不到的問題。
隱藏內(nèi)部的實(shí)現(xiàn)
對(duì)函數(shù)的傳統(tǒng)認(rèn)知就是先聲明一個(gè)函數(shù)阻星,然后再向里面添加代碼朋鞍。但反過來想可以帶來一些啟示:從所寫的代碼中挑選任意的一個(gè)片段,然后用函數(shù)聲明對(duì)它進(jìn)行包裝妥箕,實(shí)際上就是把這些代碼“隱藏”起來了滥酥。
實(shí)際的結(jié)果就是在這個(gè)代碼片段的周圍創(chuàng)建了一個(gè)作用域氣泡,也就是說這段代碼中的任何聲明(變量或者函數(shù))都將綁定在這個(gè)新創(chuàng)建的包裝函數(shù)的作用域中矾踱,而不是先前所在的作用域中。換句話說疏哗,可以把變量和函數(shù)包裹在一個(gè)函數(shù)的作用域中呛讲,然后用這個(gè)作用域來“隱藏”他們禾怠。【就相當(dāng)于在原來的集體圈一個(gè)自己的小集體出來贝搁,只能一部分和外界溝通吗氏,至于怎么溝通,由這個(gè)小集體內(nèi)部自己決定】
為什么“隱藏”變量和函數(shù)是一個(gè)有用的技術(shù)雷逆?
有很多原因促成了這種基于作用域的隱藏方法弦讽。他們大都是從最小特權(quán)中引申出來的,也叫最小授權(quán)或最小暴露原則膀哲⊥【這個(gè)應(yīng)該是防止暴露的太多,會(huì)出現(xiàn)作用域的問題某宪,就像之前提到的with關(guān)鍵字用法實(shí)例】這個(gè)原則是指在軟件設(shè)計(jì)中仿村,應(yīng)該最小限度地暴露必要內(nèi)容,而將其他內(nèi)容都“隱藏”起來兴喂,比如某個(gè)模塊或?qū)ο蟮腁PI設(shè)計(jì)蔼囊。
這個(gè)原則可以延伸到如何選擇作用域來包含變量和函數(shù)。如果所有變量和函數(shù)都在全局作用域中衣迷,那當(dāng)然在所有的內(nèi)部嵌套作用域中訪問到他們畏鼓。但這樣會(huì)破壞前面提到的最小特權(quán)原則,因?yàn)榭赡軙?huì)暴露過多的變量或函數(shù)壶谒,而這些變量或函數(shù)本應(yīng)該是私有的云矫,正確的代碼應(yīng)該是可以阻止對(duì)這些變量或函數(shù)進(jìn)行訪問的。
例如
function doSomething(a){
b = a + doSomethingElse(a * 2);
console.log(b * 3);
}
function doSomethingElse(a){
return a - 1;
}
var b;
doSomething(2); // 15
在這個(gè)代碼片段中佃迄,變量b和函數(shù)doSomethingElse應(yīng)該是doSomething內(nèi)容具體實(shí)現(xiàn)的“私有”內(nèi)容泼差。給予外部作用域?qū)和doSomethingElse的“訪問權(quán)限”不僅沒有必要,而且可能是“危險(xiǎn)”的呵俏,因?yàn)樗麄兛赡鼙挥幸饣驘o意地以非預(yù)期的方式使用堆缘,從而導(dǎo)致超出了doSomethingElse的適用條件。更“合理”的設(shè)計(jì)會(huì)將這些私有的具體內(nèi)容隱藏在doSomething內(nèi)部普碎,例如
function doSomething(a){
function doSomethingElse(a){
return a - 1;
}
var b;
b = a + doSomethingElse(a * 2);
console.log(b * 3);
}
doSomething(2); // 15
現(xiàn)在吼肥,b和doSomethingElse都無法從外部被訪問,而只能被doSomething控制麻车。功能性和最終效果都沒有受影響缀皱,但是設(shè)計(jì)上將具體內(nèi)容私有化了,設(shè)計(jì)良好的軟件都會(huì)以此進(jìn)行實(shí)現(xiàn)动猬∑《罚【這個(gè)在項(xiàng)目重構(gòu)上,也會(huì)有一席之地】
避免沖突
“隱藏”作用域中的變量和函數(shù)所帶來的另一個(gè)好處赁咙,是可以避免同名標(biāo)識(shí)符之間的沖突钮莲,【這個(gè)小編想到了同名的變量和函數(shù)】免钻,兩個(gè)標(biāo)識(shí)符可能具有相同的名字但用途卻不一樣,無意間可能造成命名沖突崔拥。沖突會(huì)導(dǎo)致變量值被意外覆蓋极舔。
例如:
function foo(){
function bar(a){
i = 3; // 修改for循環(huán)所屬作用域中的i
console.log(a + i);
}
for(var I=0;i<10;i++){
bar(I * 2); // 糟糕,無限循環(huán)了链瓦!
}
}
foo();
Bar內(nèi)部的賦值表達(dá)式i=3外意外地覆蓋了聲明在foo內(nèi)部for循環(huán)中的i拆魏。在這個(gè)例子中將會(huì)導(dǎo)致無限循環(huán),因?yàn)閕被固定設(shè)置為3慈俯,永遠(yuǎn)滿足小于10這個(gè)條件渤刃。
Bar內(nèi)部的賦值操作需要聲明一個(gè)本地變量來使用,采用任何名字都可以肥卡,var i=3溪掀;就可以滿足這個(gè)需求(同時(shí)會(huì)為i聲明一個(gè)前面提到過的“遮蔽變量”)。另外一種方法是采用一個(gè)完全不同的標(biāo)識(shí)符名稱步鉴,比如var j=3;揪胃。但是軟件設(shè)計(jì)在某種情況下可能自然而然地要求使用同樣的標(biāo)識(shí)符名稱,因此在這種情況下使用作用域來“隱藏”內(nèi)部聲明是唯一的最佳選擇氛琢『暗荩【一定程度上也為重構(gòu)提供了更多要注意的事項(xiàng)和方案】
全局命名空間
變量沖突的一個(gè)典型例子存在于全局作用域中。當(dāng)程序中加載了多個(gè)第三方庫時(shí)阳似,如果他們沒有妥善地將內(nèi)部私有的函數(shù)或變量隱藏起來骚勘,就會(huì)很容易引發(fā)沖突。
這些庫通常會(huì)在全局作用域中聲明一個(gè)名字足夠獨(dú)特的變量撮奏,通常是一個(gè)對(duì)象俏讹,這個(gè)對(duì)象被用作庫的命名空間,所有需要暴露給外界的功能都會(huì)成為這個(gè)對(duì)象(命名空間)的屬性畜吊,而不是將自己的標(biāo)識(shí)符暴露在頂級(jí)的詞法作用域中泽疆。
例如:
var MyReallyCoolLibrary = {
awesome: ’stuff’,
doSomething: function(){
},
doAnotherThing: function(){
}
}
模塊管理
另外一種避免沖突的辦法和現(xiàn)代的模塊機(jī)制很接近,就是從眾多模塊管理器中挑選一個(gè)來使用玲献。使用這些工具殉疼,任何庫都無需將標(biāo)識(shí)符加入到全局作用域中,而是通過依賴管理器的機(jī)制將庫的標(biāo)識(shí)符顯式地引入到另外一個(gè)特定的作用域中捌年。
顯而易見瓢娜,這些工具并沒有能夠違反詞法作用域規(guī)則的“神奇”功能。它們只能利用作用域的規(guī)則強(qiáng)制所有標(biāo)識(shí)符都不能注入到共享作用域中礼预,而是保持在私有眠砾、無沖突的作用域中,這樣可以有效規(guī)避掉所有的意外沖突托酸“保【就相當(dāng)于每個(gè)模塊都在自己的小盒子里伙单,大家互不干擾】
因此,只要你愿意哈肖,即使不適用任何依賴管理工具也可以實(shí)現(xiàn)相同的功效。