JavaScript變量作用域的一個問題

變量的作用域指的是變量的可見性,而生命週期則(存活期)則是從另一個角度考察變量,本文只討論變量的可見性。

[edit]先看一個“簡單”的問題

var a = 100;  
var b = true;  
var c = 200;
function test() {  
    alert(a); //此處a = ?
    alert(b); //此處b = ?
    alert(c); //此處c = ?
    var a = 200;
    alert(a); //此處a = ?
    b = false; 
    alert(b); //此處b = ?
    if (false) {
        var c = 50;
    }
    alert(c); //此處c = ?
}  
test();

[edit]JavaScript變量作用域的分類

  •  JavaScript就兩種作用域:全局(window)、函數級(function)。函數級(function)不要理解爲“塊級(大括號{} 級)”,JavaScript中沒有塊級作用域。

[edit]全局作用域

  •  定義在所有函數最外邊,使用或不使用var關鍵字定義的變量都是全局變量(當然忽略var聲明變量是不贊成的)。全局變量其實被解析成window對象的一個屬性,所以我們可以以 “window.全局變量名”方式訪問它,推薦在沒有必要的情況下直接使用變量名訪問。如下例子演示了全局變量定義最常見的方法:
var msg1 = "This is message 1";
msg2 = "This is message 2";
alert(window.msg1);  //This is message 1  使用window關鍵字訪問
alert(window.msg2);  //This is message 2
alert(msg1);  //This is message 1  省略window關鍵字訪問
alert(msg2);  //This is message 2
function otherFunction() {}  //其他一些函數或者對象聲明代碼
var otherObject = {}
  •  在函數內(局部變量運行時環境)一樣可以定義和獲取全局變量。定義的方法就是不使用var關鍵字,而在局部環境中亦可輕鬆獲得全局變量內容,直接使 用全局變量名引用即可。需要注意的是:如果函數內定義了與全局變量同名的局部變量,那麼此時函數體將優先使用自己的局部變量,如果此時你非要使用同名的全局變量,請加上window前綴。舉例如下:
var msg1 = "This is message 1";
var msg3 = "This is message 3";
function otherFunction() {
    msg2 = "This is message 2"; //不使用var關鍵字,其實也是定義一個全局變量
    msg3 = "Message 3";
    alert(msg1);  //This is message 1  (函數內可以訪問到外面定義的全局變量,再深的函數嵌套一樣能正確獲到這個全局變量,這是JavaScript閉包的其中一種體現)
    alert(msg3);  //Message 3 (局部變量msg3)
    alert(window.msg3);  //This is message 3 (使用window前綴訪問同名的全局變量msg3)
    alert(this.msg3);    //This is message 3 (因爲otherFunction() 定義在一個全局的環境中,此時otherFunction() 的this也是指window,所以你看到window.msg3是等於this.msg3的)
}
otherFunction();
//otherFunction函數外面定義的msg1和裏面定義的msg2依然是全局變量
alert(window.msg1);  //This is message 1
alert(window.msg2);  //This is message 2  msg2是在otherFunction() 裏面定義的


[edit]局部作用域

  •  使用var關鍵字,在函數體內定義的變量是局部變量,此變量能供下面所有語句塊({})及子函數使用。這個變量在這個函數裏任何地方都可以訪問到,但卻不能在這個函數的外面“直接”訪問(閉包允許間接訪問,或代理訪問,此知識點不在本文討論範圍)。舉例如下:
function showMsg() {
    if (true) {
        var msg = "This is message";
    }
    alert(msg);  //This is message
}
showMsg();
alert(typeof(msg));  //undefind
//這裏在if {}大括號內定義的變量msg還能在if外showMsg()內訪問到,但在showMsg()外則是無法訪問的
  •  父函數的變量可以被子函數訪問,但子函數的變量卻不能被父函數訪問,顯然這與我們一開始說的函數級作用域是相吻合的。舉例如下:
function showMsg() {
    var msgA = "Message A";
    this.setMsg = function(msg) {
        var msgB = "Message B";
        alert(msgA);  //Message A (子函數setMsg()可以訪問父函數showMsg()的局部變量msgA)
    }
    alert(msgB);   //msgB未定義(在父函數中不能訪問其子函數中定義的變量msgB)
}
var sm = new showMsg();
sm.setMsg("Message String");

[edit]JavaScript沒有塊級作用域

  •  JavaScript中沒有塊級作用域,即用大括號{}包含的。Java中則有。在main方法中寫入下代碼:
public static void main(String... args) {
  for(int i = 0; i < 5; i++) {
  }
    {
        int j=10;
    }
    int z = 20;
    System.out.println(i); // i不可見,語法分析時報錯,即編譯不通過
    System.out.println(j); // j不可見,語法分析時報錯,即編譯不通過
    System.out.println(z); // z可見,輸出20
}

但如果在JavaScript:

for(var i = 0; i < 5; i++) {
}
var obj = {name:"Lily"};
for(var attr in obj) {
}
{
  var j=10;
}
alert(i);//輸出4,沒有塊級作用域
alert(attr); //輸出name,沒有塊級作用域
alert(j);//輸出10,沒有塊級作用域

這也說明一個問題,避免在全局範圍內使用for循環同時聲明變量,否則會造成全局命名範圍的污染。 當然,JavaScript1.7中提出了let關鍵字聲明變量(見https://developer.mozilla.org/cn/New_in_JavaScript_1.7 ),只作用於for語句範圍。

for(let i=0;i<5;i++) {
   //todo
}
alert(i);//運行時報錯,提示i未定義

JavaScript1.7需要這樣引用

<script type="application/javascript;version=1.7"/></script>

ps:firefox2+實現了JavaScript1.7

[edit]深入理解

現在回到我們最初提出的問題:

var a = 100;  
var b = true;  
var c = 200;
function test() {  
    alert(a); //此處a爲undefined
    alert(b); //此處b = true
    alert(c); //此處c爲undefined
    var a = 200;
    alert(a); //此處a = 200
    b = false; 
    alert(b); //此處b = false
    if (false) {
        var c = 50;
    }
    alert(c); //此處c爲undefined
}  
test();
  •  爲什麼第一次alert(a)是undefined,而第一次alert(b)是true?爲什麼兩次alert(c)都是undefined?
  •  我們都明白局部變量的優先級大於全局變量,或者說內圍作用域的變量的優先級比外圍的高。當JS引擎在當前作用域找不到此變量時,它就往外圍的作用域找。不過,在這之前,有一個嚴肅的問題是,究竟當前作用域存不存在這個變量。像javascript這樣的解釋型語言,基本分爲兩個階段,編譯期(下面爲符合大多數語言的稱呼習慣,改叫預編譯)與運行期。在預編譯階段,它是用函數來劃分作用域,然後逐層爲其以 var 聲明的變量(下略稱爲var變量)與函數定義開闢內存空間,再然後對var變量進行特殊處理,統統賦初始值爲undefined。如下圖:

  

var變量所在的作用域
a test()作用域

a

c
 
b
c
預編譯後各作用域的變量的公佈狀況
  •  在上面那個例子,當前網頁擁有兩個a,一個b,兩個c,一個test函數。如果在運行期用到除此以外的東東,比如d函數e變量什麼的,就會報未定義錯誤(用eval等非正常手段生成變量與函數的情況除外),此外,它們最多出現未賦值警告。
  •  javascript的運行期是在爲var變量與函數定義分配空間後立即執行,並且是逐行往下執行的。比如上面那個例子:
var a = 100;  //爲外圍作用域的a賦值爲100
var b = true;  //爲外圍作用域的b賦值爲true
var c = 200;  //外圍作用域的c賦值爲200
function test() {  //進行test的作用域,我們簡稱爲內圍作用域。
    alert(a); //此處立即調用內圍作用域的a,這時它還沒有來得及賦值呢!不過它已經聲明過了,因此默認爲其賦值爲undefined,於是alert爲undefined
    alert(b); //此處調用b時,由於內圍作用域並沒有聲明b,所以去外圍作用域找。外圍作用域有聲明b,並且b已賦值爲true,於是alert爲true。
    alert(c); //此處調用c時,由於內圍作用域也聲明過c(第13行儘管永遠也不會被執行到,但他的var也起到了聲明變量的作用),所以也alert爲undefined
    var a = 200;  //此處將內圍作用域的a賦值爲200
    alert(a); 
    b = false;   //此處將外圍作用域的b賦值爲false
    alert(b); 
    if (false) {
        var c = 50;   //此處儘管永遠也不會執行到,但他的var也起到了聲明變量的作用
    }
    alert(c); 
}  
test();
  •  作爲對比,我們改寫一下例子:
var a = 100;  
var b = true; 
function test() {  
    alert(a); //此處a爲undefined
    alert(b); //此處b = true
    alert(c); //此處c爲undefined
    var a = 200;
    alert(a); //此處a = 200
    b = false; 
    alert(b); //此處b = false
    c = 50;
    alert(c); //此處c = 50
}  
test();
alert(c);  //此處c = 50
var c = 200;

[edit]作用域全家福

  •  各種作用域的關係如下圖:
頂層作用域(window)
Object類屬性的作用域
Object的原型的作用域
var變量所在的作用域
閉包的作用域
function()的作用域
  •  window作用域:
window.test或者test(未以var聲明的變量,瀏覽器會默認爲其加上前綴"window.")
  •  Object類屬性的作用域:
Object.test
  •  Object的原型的作用域:
Object.prototype.test
  •  var變量所在的作用域:
var test (函數外通過var聲明的變量)
  •  閉包的作用域:
(function() {
    var test
})();
  •  function()的作用域:
function() {
    var test
}
  •  思考:
Object.prototype.test = 'wrong';
var test = 'right';
(function f() {
    alert(test);
})();
Object.test = "bbb";
Object.prototype.test = 'ccc';
window.test = "aaa";
(function f() {
    alert(test);
})();
function foo(){
  foo.abc = function() { alert('def') }
  this.abc = function() { alert('xyz') }
  abc = function() { alert('@@@@@') };
  var abc = function() { alert('$$$$$$') }
}
foo.prototype.abc = function() { alert('456'); }
foo.abc = function() { alert('123');}
var f = new foo();
f.abc();
foo.abc();
abc();

[edit]需要注意的幾個地方及使用技巧

  •  爲了避免變量混亂或被覆蓋,對於局部變量的定義一定不要忘記加上var關鍵字(必要時我們要變量使用完後主動釋放它,即“變量名=null”),同時建議把所有變量集中定義在每個函數體內的開頭位置。
  •  巧用匿名函數,減少命名衝突或變量污染。如下兩段代碼其實實現了相同的功能,而第一段代碼寫法自己可以在那個匿名函數內大膽用自己想用的變量名等,不用擔心自己定義的變量覆蓋其他人定義或自己其它地方定義的變量。
//定義一個匿名函數,然後把代碼丟到這個匿名函數裏面,能有效減少命名衝突或變量污染,這是常見JS框架的做法
(function() {
    var msg = "This is message";
    alert(msg);
})();
 
document.write(msg);  //msg未定義(匿名函數外的其他方法已無法調用msg這個變量)
var msg = "This is message";
alert(msg);
  •  不建議在無須實例化的函數內使用this代替window去訪問全局變量。一般情況使用this關鍵字的函數應當作爲JavaScript類來處理。以下函數如果僅當作普通函數調用一下,就不應該出現this關鍵字,因爲這通常是去操作一個全局變量了。例子:
function clsMsg() {
    this.msg = "This is default message";
    this.showMsg = function() {
        alert(this.msg);
    }
}
sMsg = new clsMsg();
sMsg.msg = "This is new message";
sMsg.showMsg();
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章