詞法作用域
變量作用域及其生存期是所有編程語言都必須具備的基本特性。但是在JavaScript中,卻有一種新的提法,叫做詞法作用域。在《JavaScript權(quán)威指南》中,沒有對詞法作用域進行準(zhǔn)確的定義。對于詞法作用域的解釋包含三個方面的:
1.通過閱讀包含變量定義的數(shù)行源碼就能知道變量的作用域。
2.全局變量在程序中始終都是有定義的。
3.局部變量在聲明它的函數(shù)體內(nèi)以及所嵌套的函數(shù)體內(nèi)都是有定義的。
在繼續(xù)討論前,我們有必要回顧一下其他語言的關(guān)于變量及其作用域的定義。以C語言的作用域為例:
extern int i = 0;
void Fun()
{
auto int j = 0;
static int k = 0;
}
在C語言中,extern表示外部變量,表示變量的作用域為整個應(yīng)用程序,生存期為應(yīng)用程序啟動到應(yīng)用程序退出。當(dāng)在函數(shù)外部聲明變量時,extern可以省略,因為外部變量默認(rèn)便是extern類型。auto表示內(nèi)部變量,表示變量的作用域為當(dāng)前函數(shù)內(nèi)部,生存期為函數(shù)執(zhí)行期間,函數(shù)一旦返回即銷毀。static如果在函數(shù)外部聲明,和extern效果一樣,如果在函數(shù)內(nèi)部聲明,則表示變量作用域為當(dāng)前函數(shù),但生存期為整個程序運行期間。根據(jù)以上特性,上述代碼可以簡寫為如下這樣:
int i = 0;
void Fun()
{
int j = 0;
static int k = 0;
}
相比較而言,JavaScript的詞法作用域似乎更簡單,我們把上述代碼改為JavaScript版本:
var i = 0;//全局作用域
function Fun()
{
var j = 0;//函數(shù)作用域
function Kic()
{
//此處仍然可以訪問i,j
}
}
我們對比發(fā)現(xiàn),C語言的作用域和JavaScript大同小異,區(qū)別在于JavaScript允許函數(shù)嵌套定義,內(nèi)層函數(shù)仍然可以訪問外層函數(shù)中的變量。如果我們把內(nèi)層函數(shù)當(dāng)作外層函數(shù)的返回值,就會發(fā)生一種“奇特”的現(xiàn)象,即外層函數(shù)中定義的變量明明已經(jīng)出棧了,卻還能在內(nèi)部函數(shù)中訪問。這種現(xiàn)象在JavaScript中稱之為“閉包”。在之后,我們再詳細(xì)討論“閉包”,接下來,我們討論一下作用域鏈。
作用域鏈
在C語言中,一個函數(shù)的執(zhí)行過程代表一個CPU的執(zhí)行棧,函數(shù)內(nèi)的變量都是定義在CPU的執(zhí)行棧中,一旦出棧,即所有棧中的變量就都不存在了。JavaScript中的函數(shù)卻不一樣,解釋器會將JavaScript中定義的函數(shù)處理為一個一個對象,每一個對象都有一個與之關(guān)聯(lián)的作用域?qū)ο蟆.?dāng)函數(shù)需要操作變量時,會查找作用域?qū)ο蟆H绻诋?dāng)前作用域?qū)ο笾袥]有找到,即返回到上一級函數(shù)作用域?qū)ο笾胁檎?,再沒有找到則繼續(xù)返回上一級作用域?qū)ο?,直到頂級,也就是全局作用域?qū)ο?。?dāng)一個函數(shù)開始執(zhí)行,需要注意的細(xì)節(jié)是,這里的函數(shù)執(zhí)行是由解釋器解釋執(zhí)行,而具體的解釋器內(nèi)部的執(zhí)行機制對用戶來說是不可見的。不過,可以肯定的是,由于JavaScript是解釋型語言,沒有像C語言一樣最終形成機器代碼,因此,JavaScript函數(shù)執(zhí)行并沒有直接形成CPU執(zhí)行棧,最終執(zhí)行棧的生成都是由解釋器負(fù)責(zé)執(zhí)行的。解釋器在執(zhí)行完函數(shù)代碼后,并沒有直接回收掉當(dāng)前函數(shù)作用域?qū)ο螅莿h除掉了該對象的引用,從而讓垃圾回收機制對它進行回收。問題在于,當(dāng)一個函數(shù)返回,函數(shù)作用域?qū)ο蟊粍h除后,是否該對象就會被回收掉呢?根據(jù)垃圾回收機制的特點,如果這個對象已經(jīng)沒有別的引用了,則垃圾回收機制會對它進行回收。如果還有別的引用呢?比如,嵌套函數(shù)的情況,由于有作用域鏈,嵌套函數(shù)中的內(nèi)部函數(shù)很自然地?fù)碛袑ν獠亢瘮?shù)作用域的引用。如果我們又在外部函數(shù)中將內(nèi)部函數(shù)return,則系統(tǒng)會刪除掉外部函數(shù)的作用域引用,但是內(nèi)部函數(shù)仍然保持了該作用域的引用,所以垃圾回收機制仍然不會回收掉外部函數(shù)作用域?qū)ο?。我們稱這種現(xiàn)象為“閉包”。
閉包
從作用域鏈的角度去理解閉包,一切就顯得順理成章。閉包的形成是函數(shù)作用域鏈和垃圾回收機制共同作用的結(jié)果。因此在《JavaScript權(quán)威指南》中,就有這樣的論斷:“理論上來說,所有JavaScript函數(shù)都是閉包”。因為多數(shù)時候,函數(shù)作用域?qū)ο笤诤瘮?shù)返回后就刪除了,也沒有別的地方引用它,畢竟多數(shù)時候函數(shù)內(nèi)部變量只在該函數(shù)內(nèi)部有意義,外部并不需要它,因此垃圾回收機制在函數(shù)返回后不久就清除了它。但這種清除和我們上文提到的CPU執(zhí)行棧的內(nèi)存清除是完全不一樣的。這是兩種不同的機制。很多對閉包的誤解就是從這里開始的。我們需要牢記,JavaScript對函數(shù)變量的回收也是通過垃圾回收機制進行回收的,垃圾回收機制只有檢測到對象沒有任何引用了才會回收它。當(dāng)一個函數(shù)作用域?qū)ο笤诋?dāng)前函數(shù)執(zhí)行完畢后仍然有地方引用它,就產(chǎn)生了閉包。就是這么簡單。那么最后,我們來談一談一個可以改變當(dāng)前函數(shù)作用鏈的關(guān)鍵字:with。
with
with的特殊之處是它可以修改當(dāng)前函數(shù)的作用域鏈。with的作用細(xì)節(jié)是:它將操作對象附加到當(dāng)前函數(shù)作用域鏈的頂端,在它的執(zhí)行區(qū)域內(nèi),變量查找也會查找該對象的屬性,離開with的執(zhí)行區(qū)域,則作用域鏈又恢復(fù)到with之前的模樣。以下面代碼為例:
function Fun()
{
with(window.document.forms[0])
{
username = '';
age = 12;
tel = 1111;
}
}
在上述代碼中,with會將form對象附加到作用域鏈頂端,之后再with的執(zhí)行范圍內(nèi),就能夠通過變量查找的方式直接給form的屬性賦值。離開with的執(zhí)行區(qū)域,則作用域鏈上的form對象又會被刪除掉。with在某些情況下確實能節(jié)省我們的開發(fā)工作,比如上面代碼所展示的,它可以避免過長的路徑引用。但它提供的便利卻非常有限。上述代碼我們也可以用下面的代碼代替:
function Fun()
{
var form = window.document.forms[0];
form.username = '';
form.age = 12;
form.tel = 1111;
}
在實際開發(fā)中,with總是讓代碼的可讀性變得很差,而且它也不太好優(yōu)化。with的使用率不高,并不存在不可替代的使用場景,多數(shù)開發(fā)者對它的作用細(xì)節(jié)也不甚了了。因此,如果你是進行團隊開發(fā),盡量不用with,在適合用with的場景下也盡量用其他的替代方案。這倒不僅僅因為代碼優(yōu)化的問題,還可能因為你的隊友一看with就一臉懵逼,因為他從來都沒有用過。嚴(yán)格模式下,已經(jīng)禁用了with,相信它也活不了多久了。那么,忘記它吧,也沒什么大不了的。
好了,就到這里這里吧,希望你有所收獲。。。