主要內容:
1.分析JavaScript的詞法作用域的意義
2、解析變數的作用域鏈
3.變數名提升時什麼
最近在傳智播客講解JavaScript的課程,有不少朋友覺得JavaScript是如此的簡單,但是又如此的不知如何使用,因此我準備了一些內容給大家分享一下.
這個系列主要講解JavaScript的高級部分的內容,包括作用域鏈、閉包、函數調用模式、原型以及面向對象的一些東西. 在這裡不包含JavaScript的基本語法,如果需要了解基礎的同學可以到http: //net.itcast.cn裡面去下載免費的影片學習. 好了,廢話不多說,直接進入我們的正題.
一、關於區塊級作用域
說到JavaScript的變數作用域,與咱們平常使用的類別C語言不同.
例如C#中下面程式碼:
複製代碼代碼如下:
static void Main(string[] args)
{
if(true)
{
int num = 10;
}
System.Console.WriteLine(num);
}
這段程式碼如果進行編譯,是無法通過的,因為"當前上下文中不存在名稱num". 因為這裡
變數的作用域是由花括號限定的,稱為區塊級作用域.
在區塊級作用域下,所有的變數都在定義的花括號內,從定義開始到花括號結束這個
範圍內可以使用. 出了這個範圍就無法存取. 也就是說程式碼
複製代碼代碼如下:
if(true)
{
int num = 10;
System.Console.WriteLine(num);
}
這裡可以訪問,因為變數的定義與使用在同一個花括號內.
但是在JavaScript中就不一樣,JavaScript中沒有區塊級作用域的概念.
二、JavaScript中的作用域
在JavaScript中,下面程式碼:
複製代碼代碼如下:
if(true) {
var num = 10;
}
alert(num);
運行的結果是彈窗10. 那麼在JavaScript中變數的作用範圍是怎麼限定的呢?
2.1 函數限定變數作用域
在JavaScript中,只有函數可以限定一個變數的作用範圍. 什麼意思呢?
就是說,在JavaScript中,在函數裡面定義的變量,可以在函數裡面被訪問,但在函數外
無法存取. 看如下程式碼:
複製代碼代碼如下:
var func = function() {
var num = 10;
};
try {
alert(num);
} catch ( e ) {
alert( e );
}
這段程式碼在執行時,會拋出一個異常,變數num沒有定義. 也就是說,定義在函數中的變數無法
在函數外使用,當然在函數內可以隨意的使用, 即使在賦值之前. 看下面代碼:
複製代碼代碼如下:
var func = function() {
alert(num);
var num = 10;
alert(num);
};
try {
func();
} catch ( e ) {
alert( e );
}
這段程式碼運行後,不會拋出錯誤,彈窗兩次,分別是undefined 和10(至於為什麼,下文解釋).
從這裡可以看得出,變數只有在函數中可以被訪問. 同理在該函數中的函數也可以訪問.
2.2 子網域存取父域
前面說了,函數可以限定變數的作用域,那麼在函數中的函數就成為該作用域的子域. 在子域
中的程式碼可以存取到父域中的變數. 看下面程式碼:
複製代碼代碼如下:
var func = function() {
var num = 10;
var sub_func = function() {
alert(num);
};
sub_func();
};
func();
這段程式碼執行得到的結果就是10. 可以看到上文所說的變數存取情況. 但是在子網域中存取父域的
程式碼也是有條件的. 如下面程式碼:
複製代碼代碼如下:
var func = function() {
var num = 10;
var sub_func = function() {
var num = 20;
alert(num);
};
sub_func();
};
func();
這段程式碼比前面就多了一個"var num = 20;",這句程式碼在子域中,那麼子域訪問父域的情況就發
生了變化,這段程式碼列印的結果是20. 即此時子域訪問的num是子域中的變量,而不是父域中的.
由此可見訪問有一定規則可言. 在JavaScript中使用變量,JavaScript解釋器首先在當前作
用域中搜尋是否有該變數的定義,如果有,就是用這個變數;如果沒有就到父域中尋找該變數.
以此類推,直到最頂級作用域,仍然沒有找到就拋出異常"變量未定義". 看下面代碼:
複製代碼代碼如下:
(function() {
var num = 10;
(function() {
var num = 20;
(function(){
alert(num);
})()
})();
})();
這段程式碼執行後印出20. 如果將"var num = 20;"去掉,那麼列印的就是10. 同樣,如果再去掉
"var num = 10",那麼就會出現未定義的錯誤.
三、作用域鏈
有了JavaScript的作用域的劃分,那麼可以將JavaScript的存取作用域連成一個鍊式樹狀結構.
JavaScript的作用域鏈一旦能清晰的了解,那麼對於JavaScript的變數與閉包就是非常清晰的了.
以下採用繪圖的辦法,繪製作用域鏈.
3.1 繪製規則:
1) 作用域鏈就是物件的陣列
2) 全部script是0級鏈,每個物件佔一個位置
3) 凡是看到函數延伸一個鏈出來,一級級展開
4) 存取首先看當前函數,如果沒有定義往上一級鏈檢查
5) 如此往復,直到0級鏈
3.2 舉例
看下面程式碼:
複製代碼代碼如下:
var num = 10;
var func1 = function() {
var num = 20;
var func2 = function() {
var num = 30;
alert(num);
};
func2();
};
var func2 = function() {
var num = 20;
var func3 = function() {
alert(num);
};
func3();
};
func1();
func2();
以下分析這段程式碼:
-> 首先整段程式碼是一個全域作用域,可以標記為0級作用域鏈,那麼久有一個陣列
var link_0 = [ num, func1, func2 ];// 這裡用偽代碼描述
-> 在這裡func1和func2都是函數,因此引出兩個1級作用域鏈,分別為
var link_1 = { func1: [ num, func2 ] };// 這裡用偽代碼描述
var link_1 = { func2: [ num, func3 ] };// 這裡用偽代碼描述
-> 第一條1級鏈衍生出2級鏈
var link_2 = { func2: [ num ] };// 這裡用偽代碼描述
-> 第二條1級鏈中沒有定義變量,是一個空鏈,就表示為
var link_2 = { func3: [ ] };
-> 將上面程式碼整合一下,就可以將作用域鍊錶示為:
複製代碼代碼如下:
// 這裡用偽代碼描述
var link = [ // 0級鏈
num,
{ func1 : [ // 第一條1級鏈
num,
{ func2 : [ // 2級鏈
num
] }
]},
{ func2 : [ // 第二條1級鏈
num,
{ func3 : [] }
]}
];
-> 以圖像表示為
圖:01_01作用域鏈.bmp
註:將鍊式的圖用js程式碼表現出來,再有高亮顯示的時候就非常清晰了.
有了這個作用域鏈的圖,那麼就可以非常清楚的了解訪問變數是如何進行的:
在需要使用變數時,先在目前的鏈上尋找變量,如果找到就直接使用,不會
向上再找;如果沒有找到,那麼就向上一級作用域鏈尋找,直到0級作用域鏈.
如果能非常清晰的確定變數所屬的作用域鏈的級別,那麼在分析JavaScript
程式碼與使用閉包等高階JavaScript特性的時候就會非常容易(至少我是這樣哦).
三、變數名提升與函數名提升
有了作用域鏈與變數的存取規則,那麼就有一個非常棘手的問題. 先看下面
的JavaScript程式碼:
複製代碼代碼如下:
var num = 10;
var func = function() {
alert(num);
var num = 20;
alert(num);
};
func();
執行結果會是什麼呢?你可以想一想,我先不揭曉答案.
先來分析一下這段程式碼.
這段程式碼中有一條0級作用域鏈,裡面有成員num和func. 在func下是1級作用
域鏈,裡面有成員num. 因此在呼叫函數func的時候,就會偵測到在目前作用域中
變數num是定義過的,所以就會使用這個變數. 但是此時num並沒有賦值,因為代
碼是從上往下運行的. 因此第一次打印的是undefined,而第二次打印的便是20.
你答對了麼?
像這樣將程式碼定義在後面,而在前面使用的情況在JavaScript中也是常見的
問題. 這時就好像變數在一開始就定義了一樣,結果就如同下面程式碼:
複製代碼代碼如下:
var num = 10;
var func = function() {
var num;// 感覺就是這裡已經定義了,但是沒有賦值一樣
alert(num);
var num = 20;
alert(num);
};
func();
那麼這個現象常常稱為變數名稱提升. 同樣也有函數名稱提升這一說. 如下面程式碼:
複製代碼代碼如下:
var func = function() {
alert("呼叫外面的函數");
};
var foo = function() {
func();
var func = function() {
alert("呼叫內部的函數");
};
func();
};
好了,這段程式碼結果如何?或應該有什麼不一樣,我先不說沒留著讀者思考吧!
下一篇再做解答.
由於有了這些不同,因此在實際開發的時候,推薦將變數都寫在開始的地方,
也就是在函數的開頭將變數就定義好,類似C語言的規定一樣. 這個在js庫中也
是這麼完成的,如jQuery等.
四、小結
好了這篇文章主要是說明JavaScript的詞法作用域是怎麼一回事兒,以及解釋
如何分析作用域鏈,和變數的存取情況,最後留再一個練習收尾吧! ! !
看下面程式碼執行結果是什麼:
複製代碼代碼如下:
if( ! "a" in window ) {
var a = "定義變數";
}
alert(a);