JavaScript中的詞法作用域

詞法作用域

變量作用域及其生存期是所有編程語言都必須具備的基本特性。但是在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,相信它也活不了多久了。那么,忘記它吧,也沒什么大不了的。
好了,就到這里這里吧,希望你有所收獲。。。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容