Javascript 這門語言與其他的大部分語言相比,有很多特殊性,這是很多人喜歡它或者討厭它的原因。其中變數的作用域問題,對很多初學者來說就是一個又一個「坑」。
變數的作用域在程式設計技能中算是一個基本概念,而在 Javascript 中,這一基本概念往往挑戰者初學者的常識。
基本的變數作用域
先上例子:
var scope = ‘global’;
function checkScope(){
var scope = ‘local’;
console.log(scope);
// local
}
checkScope();
console.log(scope);
// global
上面的例子中,宣告了全域性變數 scope 和函式體內的區域性變數 scope。在函式體內部,區域性變數的優先順序比通明的全域性變數要高,如果一個區域性變數的名字與一個全域性變數相同,那麼,在宣告區域性變數的函式體範圍內,區域性變數將改寫同名的全域性變數。
下麵再看一個例子:
scope = ‘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 變數?
這兩個問題都源於一個特性。在全域性作用域中宣告變數可以省略 var 關鍵字,但是如果在函式體內宣告變數時不使用 var關鍵字,就會發生上面的現象。首先,函式體內的第一行陳述句,把全域性變數中的 scope 變數的值改變了。而在宣告myScope 變數時,由於沒有使用 var 關鍵字,Javascript 就會在全域性範圍內宣告這個變數。因此,在宣告區域性變數時使用var 關鍵字是個很好的習慣。
在 Javascript 中,沒有「塊級作用域」一說
在 C 或者 Java 等語言中,if、for 等陳述句塊內可以包含自己的區域性變數,這些變數的作用域是這些陳述句的陳述句塊,而在 Javascript 中,不存在「塊級作用域」的說法。
看下麵的例子:
function 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 中不存在塊級作用域,因此,函式中宣告的所有變數,無論是在哪裡宣告的,在整個函式中它們都是有定義的。
如果要更加強調上文中 函式中宣告的所有變數,無論是在哪裡宣告的,在整個函式中它們都是有定義的 這句話,那麼還可以在後面跟一句話:函式中宣告的所有變數,無論是在哪裡宣告的,在整個函式中它們都是有定義的,即使是在宣告之前。對於這句話,有個經典的困擾初學者的「坑」。
var a = 2;
function test(){
console.log(a);
var a = 10;
}
test();
上面的例子中,控制檯輸出變數 a 的值為 undefined,既不是全域性變數 a 的值 2,也不是區域性變數 a 的值 10。首先,區域性變數在整個函式體內都是有定義的,因此,區域性變數 a 會在函式體內改寫全域性變數 a,而在函式體內,在 var 陳述句之前,它是不會被初始化的。如果要讀取一個未被初始化的變數,將會得到一個預設值 undefined。
所以,上面示例中的程式碼與下麵的程式碼時等價的:
var 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 的函式是可以巢狀的,因此每個函式執行環境的作用域鏈可能包含不同數目個物件,一個非巢狀的函式的執行環境中,作用域鏈包含了這個函式的呼叫物件和全域性物件,而在巢狀的函式的執行環境中,作用域鏈包含了巢狀的每一層函式的呼叫物件以及全域性變數。
我們可以用一個圖來直觀地解釋作用域鏈和變數名解析的過程:
來自:segmentfault-範斌
連結:http://segmentfault.com/a/1190000002960647