JavaScript高級之詞法作用域和作用域鏈


主要內容:

  1. 分析JavaScript的詞法作用域的含義
  2. 解析變量的作用域鏈
  3. 變量名提升時什么

  最近在傳智播客講解JavaScript的課程,有不少朋友覺得JavaScript是如此的簡單, 但是又如此的不知如何使用,因此我准備了一些內容給大家分享一下.
  這個系列主要講解JavaScript的高級部分的內容,包括作用域鏈、閉包、函數調用 模式、原型以及面向對象的一些東西. 在這里不包含JavaScript的基本語法,如果需要
了解基礎的同學可以到http://net.itcast.cn里面去下載免費的視頻進行學習. 好了,廢話不多說,直接進入我們的正題.

一、關於塊級作用域

  說到JavaScript的變量作用域,與咱們平時使用的類C語言不同. 例如C#中下面代碼:

1 static void Main(string[] args)
2 {
3     if(true)
4     {
5         int num = 10;
6     }
7     System.Console.WriteLine(num);
8 }

 

這段代碼如果進行編譯,是無法通過的,因為"當前上下文中不存在名稱num". 因為這里變量的作用域是由花括號限定的,稱為塊級作用域.

在塊級作用域下,所有的變量都在定義的花括號內,從定義開始到花括號結束這個 范圍內可以使用. 出了這個范圍就無法訪問. 也就是說代碼

1 if(true)
2 {
3     int num = 10;
4     System.Console.WriteLine(num);
5 }

 

這里可以訪問,因為變量的定義與使用在同一個花括號內.

但是在JavaScript中就不一樣,JavaScript中沒有塊級作用域的概念.

 

二、JavaScript中的作用域

在JavaScript中,下面代碼:

1 if(true) {
2     var num = 10;
3 }

alert(num); 運行的結果是彈窗10. 那么在JavaScript中變量的作用范圍是怎么限定的呢?

2.1 函數限定變量作用域

  在JavaScript中,只有函數可以限定一個變量的作用范圍. 什么意思呢? 就是說,在JavaScript中,在函數里面定義的變量,可以在函數里面被訪問,但是在函數外
無法訪問. 看如下代碼:

1 var func = function() {
2     var num = 10;
3 };
4 try {
5     alert(num);
6 } catch ( e ) {
7     alert( e );
8 }

這段代碼運行時,會拋出一個異常,變量num沒有定義. 也就是說,定義在函數中的變量無法在函數外使用,當然在函數內可以隨意的使用, 即使在賦值之前. 看下面代碼:

 1 var func = function() {
 2     alert(num);
 3     var num = 10;
 4     alert(num);
 5 };
 6 try {
 7     func();
 8 } catch ( e ) {
 9     alert( e );
10 }

這段代碼運行后,不會拋出錯誤,彈窗兩次,分別是 undefined 和 10(至於為什么,下文解釋).

從這里可以看得出,變量只有在函數中可以被訪問. 同理在該函數中的函數也可以訪問.

 

2.2 子域訪問父域

  前面說了,函數可以限定變量的作用域,那么在函數中的函數就成為該作用域的子域. 在子域 中的代碼可以訪問到父域中的變量. 看下面代碼:

1 var func = function() {
2     var num = 10;
3     var sub_func = function() {
4         alert(num);
5     };
6     sub_func();
7 };
8 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解釋器首先在當前作 用域中搜索是否有該變量的定義,如果有,就是用這個變量;如果沒有就到父域中尋找該變量. 以此類推,直到最頂級作用域,仍然沒有找到就拋出異常"變量未定義". 看下面代碼:

1 (function() {
2     var num = 10;
3     (function() {
4         var num = 20;
5         (function(){
6             alert(num);
7         })()
8     })();
9 })();    

這段代碼執行后打印出20. 如果將"var num = 20;"去掉,那么打印的就是10. 同樣,如果再去掉"var num = 10",那么就會出現未定義的錯誤.

三、作用域鏈

  有了JavaScript的作用域的划分,那么可以將JavaScript的訪問作用域連成一個鏈式樹狀結構. JavaScript的作用域鏈一旦能清晰的了解,那么對於JavaScript的變量與閉包就是非常清晰的了.

下面采用繪圖的辦法,繪制作用域鏈.

3.1 繪制規則:

1) 作用域鏈就是對象的數組
2) 全部script是0級鏈,每個對象占一個位置
3) 凡是看到函數延伸一個鏈出來,一級級展開
4) 訪問首先看當前函數,如果沒有定義往上一級鏈檢查
5) 如此往復,直到0級鏈

3.2 舉例

看下面代碼:

 1 var num = 10;
 2 var func1 = function() {
 3     var num = 20;
 4     var func2 = function() {
 5         var num = 30;
 6         alert(num);
 7     };
 8     func2();
 9 };
10 var func2 = function() {
11     var num = 20;
12     var func3 = function() {
13         alert(num);
14     };
15     func3();
16 };
17 func1();
18 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: [ ] };
-> 將上面代碼整合一下,就可以將作用域鏈表示為:

 1 // 這里用偽代碼描述
 2 var link = [ // 0級鏈
 3     num,
 4     { func1 : [    // 第一條1級鏈
 5         num,
 6         { func2 : [    // 2級鏈
 7             num
 8         ] }
 9     ]},
10     { func2 : [    // 第二條1級鏈
11         num,
12         { func3 : [] }
13     ]}
14 ];    

-> 用圖像表示為

圖:01_01作用域鏈.bmp
注意:將鏈式的圖用js代碼表現出來,再有高亮顯示的時候就非常清晰了.

   有了這個作用域鏈的圖,那么就可以非常清晰的了解訪問變量是如何進行的:
    在需要使用變量時,首先在當前的鏈上尋找變量,如果找到就直接使用,不會向上再找;如果沒有找到,那么就向上一級作用域鏈尋找,直到0級作用域鏈.

  如果能非常清晰的確定變量所屬的作用域鏈的級別,那么在分析JavaScript 代碼與使用閉包等高級JavaScript特性的時候就會非常容易(至少我是這樣哦).


三、變量名提升與函數名提升

有了作用域鏈與變量的訪問規則,那么就有一個非常棘手的問題. 先看下面 的JavaScript代碼:

1 var num = 10;
2 var func = function() {
3     alert(num);
4     var num = 20;
5     alert(num);
6 };
7 func();

 

執行結果會是什么呢?你可以想一想,我先不揭曉答案.

先來分析一下這段代碼.
  這段代碼中有一條0級作用域鏈,里面有成員num和func. 在func下是1級作用 域鏈,里面有成員num. 因此在調用函數func的時候,就會檢測到在當前作用域中
變量num是定義過的,所以就會使用這個變量. 但是此時num並沒有賦值,因為代碼是從上往下運行的. 因此第一次打印的是 undefined,而第二次打印的便是20.
  你答對了么?
  

  像這樣將代碼定義在后面,而在前面使用的情況在JavaScript中也是常見的 問題. 這時就好像變量在一開始就定義了一樣,結果就如同下面代碼:

1 var num = 10;
2     var func = function() {
3     var num;    // 感覺就是這里已經定義了,但是沒有賦值一樣
4     alert(num);
5     var num = 20;
6     alert(num);
7 };
8 func();

 


那么這個現象常常稱為變量名提升. 同樣也有函數名提升這一說. 如下面代碼:

 1 var func = function() {
 2     alert("調用外面的函數");
 3 };
 4 var foo = function() {
 5     func();
 6 
 7     var func = function() {
 8     alert("調用內部的函數");
 9     };
10 
11     func();
12 };
13 foo();

 

 好了,這段代碼結果如何?或則應該有什么不一樣,我先不說沒留着讀者思考吧! 下一篇再做解答.

  由於有了這些不同,因此在實際開發的時候,推薦將變量都寫在開始的地方, 也就是在函數的開頭將變量就定義好,類似於C語言的規定一樣. 這個在js庫中也
是這么完成的,如jQuery等.


四、小結

  好了這篇文章主要是說明JavaScript的詞法作用域是怎么一回事兒,以及解釋 如何分析作用域鏈,和變量的訪問情況,最后留再一個練習收尾吧!!!

看下面代碼執行結果是什么:

1 if(! "a" in window) {
2     var a = "定義變量";
3 }
4 alert(a);
5 
6  

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM