理解 Javascript 中變量的做用域

Javascript 這門語言與其餘的大部分語言相比,有不少特殊性,這是不少人喜歡它或者討厭它的緣由。其中變量的做用域問題,對不少初學者來講就是一個又一個「坑」。javascript

變量的做用域在編程技能中算是一個基本概念,而在 Javascript 中,這一基本概念每每挑戰者初學者的常識。java

基本的變量做用域

先上例子:編程

javascriptvar scope = 'global';
function checkScope(){
    var scope = 'local';
    console.log(scope); // local
}
checkScope();
console.log(scope); // global

上面的例子中,聲明瞭全局變量 scope 和函數體內的局部變量 scope。在函數體內部,局部變量的優先級比通明的全局變量要高,若是一個局部變量的名字與一個全局變量相同,那麼,在聲明局部變量的函數體範圍內,局部變量將覆蓋同名的全局變量。函數

下面再看一個例子:spa

javascriptscope = 'global';
function checkScope(){
    scope = 'local'; 
    console.log(scope); // local
    myScope = 'local';
    console.log(myScope); // local
}
checkScope();
console.log(scope); // local
console.log(myScope); // local

對於初學者來講,可能會有兩個疑問:爲何在函數體外,scope 的值也變成了 local ?爲何在函數體外能夠訪問 myScope 變量?code

這兩個問題都源於一個特性。在全局做用域中聲明變量能夠省略 var 關鍵字,可是若是在函數體內聲明變量時不使用 var 關鍵字,就會發生上面的現象。首先,函數體內的第一行語句,把全局變量中的 scope 變量的值改變了。而在聲明 myScope 變量時,因爲沒有使用 var 關鍵字,Javascript 就會在全局範圍內聲明這個變量。所以,在聲明局部變量時使用 var 關鍵字是個很好的習慣。對象

在 Javascript 中,沒有「塊級做用域」一說

在 C 或者 Java 等語言中,iffor 等語句塊內能夠包含本身的局部變量,這些變量的做用域是這些語句的語句塊,而在 Javascript 中,不存在「塊級做用域」的說法。blog

看下面的例子:ip

javascriptfunction checkScope(obj){
    var i = 0;
    if (typeof obj == 'object') {
        var j = 0;
        for (var k = 0; k < 10; k++) {
            console.log(k);
        }
        console.log(k);
    }
    console.log(j);
}
checkScope(new Object());

在上面的例子中,每一條控制檯輸出語句都能輸出正確的值,這是由於,因爲 Javascript 中不存在塊級做用域,所以,函數中聲明的全部變量,不管是在哪裏聲明的,在整個函數中它們都是有定義的作用域

若是要更增強調上文中 函數中聲明的全部變量,不管是在哪裏聲明的,在整個函數中它們都是有定義的 這句話,那麼還能夠在後面跟一句話:函數中聲明的全部變量,不管是在哪裏聲明的,在整個函數中它們都是有定義的,即便是在聲明以前。對於這句話,有個經典的困擾初學者的「坑」。

javascriptvar a = 2;
function test(){
    console.log(a);
    var a = 10;
}
test();

上面的例子中,控制檯輸出變量 a 的值爲 undefined,既不是全局變量 a 的值 2,也不是局部變量 a 的值 10。首先,局部變量在整個函數體內都是有定義的,所以,局部變量 a 會在函數體內覆蓋全局變量 a,而在函數體內,var 語句以前,它是不會被初始化的。若是要讀取一個未被初始化的變量,將會獲得一個默認值 undefined

因此,上面示例中的代碼與下面的代碼時等價的:

javascriptvar a = 2;
function test(){
    var a;
    console.log(a);
    a = 10;
}
test();

可見,把全部的函數聲明集合起來放在函數的開頭是個良好的習慣

變量的真相

可能不少人已經注意到,在 Javascript 當中,一個變量與一個對象的一個屬性,有不少類似的地方,實際上,它們並無什麼本質區別。在 Javascript 中,任何變量都是某個特定對象的屬性

全局變量都是全局對象的屬性。在 Javascript 解釋器開始運行且沒有執行 Javascript 代碼以前,會有一個「全局對象」被建立,而後 Javascript 解釋器會給它與定義一些屬性,這些屬性就是咱們在 Javascript 代碼中能夠直接使用的內置的變量和方法。以後,每當咱們定義一個全局變量,其實是給全局對象定義了一個屬性。

在客戶端的 Javascript 當中,這個全局變量就是 Window 對象,它有一個指向本身的屬性 window ,這就是咱們經常使用的全局變量。

對於函數的局部變量,則是在函數開始執行時,會有一個對應的「調用對象」被建立,函數的局部變量都做爲它的屬性而存儲。這樣能夠防止局部變量覆蓋全局變量。

做用域鏈

若是要深刻理解 Javascript 中變量的做用域,那就必須拿出「做用域鏈」這個終極武器。

首先要理解的一個名詞就是「執行環境」,每當 Javascript 執行時,都會有一個對應的執行環境被建立,這個執行環境中很重要的一部分就是函數的調用對象(前面說過,調用對象是用來存儲相應函數的局部變量的對象),每個 Javascript 方法都是在本身獨有的執行環境中運行的。簡而言之,函數的執行環境包含了調用對象,調用對象的屬性就是函數的局部變量,每一個函數就是在這樣的執行環境中執行,而在函數以外的代碼,也在一個執行環境中,這個執行環境包含了全局變量。

在 Javascript 的執行環境中,還有一個與之對應的「做用域鏈」,它是一個由對象組成的列表或鏈。

當 Javascript 代碼須要查詢一個變量 x 的時候,會有一個被稱爲「變量名解析」的過程。它會首先檢查做用域鏈的第一個對象,若是這個對象包含名爲 x 的屬性,那麼就採用這個屬性的值,不然,會繼續檢查第二個對象,依此類推。當檢查到最後一個對象的時候仍然沒有相應的屬性,則這個變量會被認定爲是「未定義」的。

在全局的 Javascript 執行環境中,做用域鏈中只包含一個對象,就是全局對象。而在函數的執行環境中,則同時包含函數的調用對象。因爲 Javascript 的函數是能夠嵌套的,所以每一個函數執行環境的做用域鏈可能包含不一樣數目個對象,一個非嵌套的函數的執行環境中,做用域鏈包含了這個函數的調用對象和全局對象,而在嵌套的函數的執行環境中,做用域鏈包含了嵌套的每一層函數的調用對象以及全局變量。

咱們能夠用一個圖來直觀地解釋做用域鏈和變量名解析的過程:

Javascript 做用域鏈he變量名解析

相關文章
相關標籤/搜索