Javascript 這門語言與其他的大部分語言相比,有很多特殊性,這是很多人喜歡它或者討厭它的原因员舵。其中變量的作用域問題,對很多初學(xué)者來說就是一個又一個「坑」藕畔。
變量的作用域在編程技能中算是一個基本概念,而在 Javascript 中庄拇,這一基本概念往往挑戰(zhàn)者初學(xué)者的常識注服。
基本的變量作用域
先上例子:
var scope = 'global';
function checkScope(){
var scope = 'local';
console.log(scope); // local
}
checkScope();
console.log(scope); // global
上面的例子中,聲明了全局變量 scope
和函數(shù)體內(nèi)的局部變量 scope
措近。在函數(shù)體內(nèi)部溶弟,局部變量的優(yōu)先級比通明的全局變量要高,如果一個局部變量的名字與一個全局變量相同瞭郑,那么辜御,在聲明局部變量的函數(shù)體范圍內(nèi),局部變量將覆蓋同名的全局變量屈张。
下面再看一個例子:
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
對于初學(xué)者來說擒权,可能會有兩個疑問:為什么在函數(shù)體外,scope
的值也變成了 local
阁谆?為什么在函數(shù)體外可以訪問 myScope
變量碳抄?
這兩個問題都源于一個特性。在全局作用域中聲明變量可以省略 var
關(guān)鍵字场绿,但是如果在函數(shù)體內(nèi)聲明變量時不使用 var
關(guān)鍵字剖效,就會發(fā)生上面的現(xiàn)象。首先焰盗,函數(shù)體內(nèi)的第一行語句璧尸,把全局變量中的 scope
變量的值改變了。而在聲明 myScope
變量時熬拒,由于沒有使用 var
關(guān)鍵字爷光,Javascript 就會在全局范圍內(nèi)聲明這個變量。因此梦湘,在聲明局部變量時使用 var
關(guān)鍵字是個很好的習(xí)慣瞎颗。
在 Javascript 中件甥,沒有「塊級作用域」一說
在 C 或者 Java 等語言中,if
哼拔、for
等語句塊內(nèi)可以包含自己的局部變量引有,這些變量的作用域是這些語句的語句塊,而在 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 中不存在塊級作用域,因此健民,函數(shù)中聲明的所有變量抒巢,無論是在哪里聲明的,在整個函數(shù)中它們都是有定義的秉犹。
如果要更加強(qiáng)調(diào)上文中 函數(shù)中聲明的所有變量蛉谜,無論是在哪里聲明的,在整個函數(shù)中它們都是有定義的
這句話崇堵,那么還可以在后面跟一句話:函數(shù)中聲明的所有變量型诚,無論是在哪里聲明的,在整個函數(shù)中它們都是有定義的鸳劳,即使是在聲明之前狰贯。對于這句話,有個經(jīng)典的困擾初學(xué)者的「坑」赏廓。
var a = 2;
function test(){
console.log(a);
var a = 10;
}
test();
上面的例子中涵紊,控制臺輸出變量 a
的值為 undefined
,既不是全局變量 a
的值 2
,也不是局部變量 a
的值 10
楚昭。首先栖袋,局部變量在整個函數(shù)體內(nèi)都是有定義的,因此抚太,局部變量 a
會在函數(shù)體內(nèi)覆蓋全局變量 a
塘幅,而在函數(shù)體內(nèi),在 var
語句之前尿贫,它是不會被初始化的电媳。如果要讀取一個未被初始化的變量,將會得到一個默認(rèn)值 undefined
庆亡。
所以匾乓,上面示例中的代碼與下面的代碼時等價的:
var a = 2;
function test(){
var a;
console.log(a);
a = 10;
}
test();
可見,把所有的函數(shù)聲明集合起來放在函數(shù)的開頭是個良好的習(xí)慣又谋。
變量的真相
可能很多人已經(jīng)注意到拼缝,在 Javascript 當(dāng)中娱局,一個變量與一個對象的一個屬性,有很多相似的地方咧七,實際上衰齐,它們并沒有什么本質(zhì)區(qū)別。在 Javascript 中继阻,任何變量都是某個特定對象的屬性耻涛。
全局變量都是全局對象的屬性。在 Javascript 解釋器開始運(yùn)行且沒有執(zhí)行 Javascript 代碼之前瘟檩,會有一個「全局對象」被創(chuàng)建抹缕,然后 Javascript 解釋器會給它與定義一些屬性,這些屬性就是我們在 Javascript 代碼中可以直接使用的內(nèi)置的變量和方法墨辛。之后卓研,每當(dāng)我們定義一個全局變量,實際上是給全局對象定義了一個屬性睹簇。
在客戶端的 Javascript 當(dāng)中鉴分,這個全局變量就是 Window
對象,它有一個指向自己的屬性 window
带膀,這就是我們常用的全局變量。
對于函數(shù)的局部變量橙垢,則是在函數(shù)開始執(zhí)行時垛叨,會有一個對應(yīng)的「調(diào)用對象」被創(chuàng)建,函數(shù)的局部變量都作為它的屬性而存儲柜某。這樣可以防止局部變量覆蓋全局變量嗽元。
作用域鏈
如果要深入理解 Javascript 中變量的作用域,那就必須拿出「作用域鏈」這個終極武器喂击。
首先要理解的一個名詞就是「執(zhí)行環(huán)境」剂癌,每當(dāng) Javascript 執(zhí)行時,都會有一個對應(yīng)的執(zhí)行環(huán)境被創(chuàng)建翰绊,這個執(zhí)行環(huán)境中很重要的一部分就是函數(shù)的調(diào)用對象(前面說過佩谷,調(diào)用對象是用來存儲相應(yīng)函數(shù)的局部變量的對象),每一個 Javascript 方法都是在自己獨(dú)有的執(zhí)行環(huán)境中運(yùn)行的监嗜。簡而言之谐檀,函數(shù)的執(zhí)行環(huán)境包含了調(diào)用對象,調(diào)用對象的屬性就是函數(shù)的局部變量裁奇,每個函數(shù)就是在這樣的執(zhí)行環(huán)境中執(zhí)行桐猬,而在函數(shù)之外的代碼,也在一個執(zhí)行環(huán)境中刽肠,這個執(zhí)行環(huán)境包含了全局變量溃肪。
在 Javascript 的執(zhí)行環(huán)境中免胃,還有一個與之對應(yīng)的「作用域鏈」,它是一個由對象組成的列表或鏈惫撰。
當(dāng) Javascript 代碼需要查詢一個變量 x
的時候羔沙,會有一個被稱為「變量名解析」的過程。它會首先檢查作用域鏈的第一個對象润绎,如果這個對象包含名為 x
的屬性撬碟,那么就采用這個屬性的值,否則莉撇,會繼續(xù)檢查第二個對象呢蛤,依此類推。當(dāng)檢查到最后一個對象的時候仍然沒有相應(yīng)的屬性棍郎,則這個變量會被認(rèn)定為是「未定義」的其障。
在全局的 Javascript 執(zhí)行環(huán)境中,作用域鏈中只包含一個對象涂佃,就是全局對象励翼。而在函數(shù)的執(zhí)行環(huán)境中,則同時包含函數(shù)的調(diào)用對象辜荠。由于 Javascript 的函數(shù)是可以嵌套的汽抚,因此每個函數(shù)執(zhí)行環(huán)境的作用域鏈可能包含不同數(shù)目個對象,一個非嵌套的函數(shù)的執(zhí)行環(huán)境中伯病,作用域鏈包含了這個函數(shù)的調(diào)用對象和全局對象造烁,而在嵌套的函數(shù)的執(zhí)行環(huán)境中,作用域鏈包含了嵌套的每一層函數(shù)的調(diào)用對象以及全局變量午笛。
我們可以用一個圖來直觀地解釋作用域鏈和變量名解析的過程: