(好文推薦)一篇文章看懂JavaScript做用域鏈

閉包和做用域鏈是JavaScript中比較重要的概念,首先,看看幾段簡單的代碼。javascript

代碼1:html

 1 var name = "stephenchan";
 2   var age = 23;
 3   function myFunc() {
 4       alert(name);
 5       var name = "endlesscode";
 6       alert(name);
 7       alert(age);
 8       alert(weight);
 9   }
10  myFunc();
11  myFunc();

 

上述代碼1中,兩次調用myFunc()的輸出是一致的。可能你會認爲輸出是:java

  stephenchan
  endlesscode
  23
  [Reference Error]

 

可是結果倒是:編程

  代碼1輸出:
  undefined
  endlesscode
  23
  [Reference Error]

 

代碼2:數據結構

 1  var i = 10;
 2   function myFunc() {
 3       var i = 20;
 4       function innerFunc() {
 5           alert(i);
 6       }
 7       return innerFunc;
 8   }
 9   var func = myFunc();
10  func();

 

上面的代碼2會輸出20,但爲何不輸出10或者是輸出undefined?閉包

代碼3:less

 1  var name = "stephenchan";
 2   function callMePlz() {
 3       alert(name);
 4   }
 5   
 6   function myFunc() {
 7       var name = "endlesscode";
 8       callMePlz();
 9   }
10  
11  myFunc();

 

上面的代碼3輸出的會是endlesscode、stephenchan仍是undefined?函數

  代碼3輸出:
  stephenchan

代碼4:this

 1 function callMePlz() {
 2       var name = "stephenchan";
 3       var intro = function() {
 4           alert("I am " + name);
 5       }
 6       return intro;
 7   }
 8   
 9   function showMe(arg) {
10      var name = arg;
11      var func = callMePlz();
12      func();
13  }
14  showMe("endlesscode");

 

上面的代碼4與代碼3不一樣的是,從callMePlz返回的函數引用,而後再執行函數。spa

  代碼4輸出:
  I am stephenchan

 

代碼5:

 1 var name = "stephenchan";
 2   function callMePlz() {
 3       var intro = function() {
 4           alert("I am " + name);
 5       }
 6       return intro;
 7   }
 8   
 9   function showMe(arg) {
10      var name = arg;
11      var func = callMePlz();
12      func();
13  }
14  showMe("endlesscode");

 

上面的代碼5與代碼4不一樣的是原來在callMePlz函數中的變量name在全局環境中聲明瞭,但輸出的結果是:

  代碼5輸出:
  I am stephenchan

  

先不對上面的代碼進行說明,講述一下閉包和做用域鏈的概念。

閉包(closure)是什麼?閉包與函數有着緊密的關係。「在JavaScript中,一個函數只是一段靜態的代碼、腳本文件,所以函數是一個代碼書寫時,以及編譯期的、靜態的概念;而閉包是函數的代碼在運行過程當中的一個動態環境,是一個運行期的、動態的概念」。這是《JavaScript語言精髓和編程實踐》中對函數和閉包的描述,實際上咱們常說的閉包卻是能夠表現爲如上面代碼2中的innerFunc同樣,在myFunc的函數執行後返回的是一個在其內部定義的、外部可調用的函數引用,這個函數語言的特性在C和C++是沒有的。爲何在myFunc結束以後innerFunc還能正常訪問到myFunc裏面的數據呢?這就涉及到函數執行環境與閉包的相關概念,閉包中所保留着函數運行的實例,環境以及做用域鏈等等,並在myFunc調用以後沒有將函數實例直接丟棄,所以在調用innerFunc的時候可以引用到myFunc中聲明的i。

做用域鏈(scope chain)是什麼?顧名思義,就是由做用域組成的鏈,是一個相似鏈狀的數據結構。做用域就是對上下文環境的數據描述。閉包和做用域鏈是緊密關係的,函數實例執行時的閉包是構成做用域鏈的基本元素。JavaScript代碼在執行前會進行語法分析,在語法分析階段,會記錄全局環境中的變量聲明和函數定義,構造函數的調用對象(Call Oject、Activation Object、Activate Object、活動對象,不一樣稱呼罷了)和在全局環境下的做用域鏈。

 

圖1

 

圖1是《JavaScript語言精髓和編程實踐》一書中對閉包相關元素的內部數據結構的描述。咱們主要關注其中的ScriptObject,ScriptObject是對調用對象的一種描述。ScriptObject在語法分析階段就已經構造好了,其中的varDecls是保存着函數中的變量聲明,funcDecls保存着內部的函數聲明。

在語法分析階段,varDecls保存在函數中用var進行顯示聲明的局部變量,而且置默認值爲undefined,這裏就是在代碼1中"alert(name)"輸出爲undefined的緣由,因爲myFunc在語法分析階段就已經保留了標記符name在varDecls,在賦值語句"var name='endlesscode'"執行以前name的值都是undefined,所以在"alert(name)"的時候就顯示爲undefined了。

而函數定義在語法分析階段工做就稍微有點不一樣。在語法分析階段,發現有函數定義的時候,除了記錄函數的聲明外,還會建立一個函數對象,並將當前的做用域鏈賦值給此函數對象的[[scope]]屬性(這個屬性是JavaScript引擎內部維護的,可是Firefox倒是能夠經過私有屬性__parent__來訪問它),這裏要注意的是在語法分析階段將做用域鏈賦值給[[scope]]屬性,而不是在執行階段。若是是在全局環境下,但當前的做用域鏈爲只有一個元素,就是全局的調用對象(Windows Object, Global Object,不一樣稱呼罷了)。這就是爲何在《JavaScript權威指南》中提到「JavaScript中的函數運行在它們被定義的做用域裏,而不是它們被執行的做用域裏。」

 

下面以一段代碼的大概處理流程來進行說明:

1 var outerVar1 = "var in global code";
2  function outerFunc(arg1, arg2) {
3      var innerVar1 = "var in function code";
4      function innerFunc() { return outerVar1 + "-" + innerVar1 + "-" + (arg1 + arg2); }
5      return innerFunc();
6  }
7  var outerVar2 = outerFunc(10, 20);

執行處理過程大體以下:(精華1)

  1. 引擎啓動,初始化Global Object,即window對象,全局的調用對象,創建做用域鏈,假設爲scope_1,做用域鏈中只包含全局的上下文環境,即Global Object。
  2. 語法分析階段,掃描JavaScript代碼,獲取代碼中變量和函數定義,其掃描過程以下:全局環境下語法分析結束,執行outerVar1賦值語句,賦值爲"var in global code"。
    1. 發現變量outerVar1,在Global Object的varDecls中添加outerVar1屬性,值爲undefined。
    2. 發現函數outerFunc的定義,在Global Object的funcDecls中添加outerFunc,並建立函數對象(這裏應該建立的是函數的原型對象),將scope_1傳遞給outerFunc的函數對象,即outerFunc內部的[[scope]]屬性。另外注意,建立過程並不會對函數體中的JavaScript代碼作特殊處理,能夠理解爲只是將函數JavaScript代碼保存中函數對象的內部屬性上,在函數執行時再作進一步處理。也就是說,這一步大概處理的就是記錄函數定義,賦值[[scope]]屬性記錄當前定義的做用域,而沒有進一步對outerFunc裏面的代碼進行進一步的語法分析。
    3. 發現變量outerVar2,在Global Object中的varDecls中添加屬性,值爲undefined。
  3. 全局環境下語法分析結束,執行outerVar1賦值語句,賦值爲"var in global code"。
  4. 執行outerFunc,獲取返回值。將outerFunc的返回結果賦值給outerVar2。
    1. 建立調用對象,假設爲act_obj_1。同時將act_obj_1連接起outerFunc的[[scope]]屬性,構成一個新的做用域鏈,假設爲scope_2,scope_2中的第一個對象爲act_obj_1,act_obj_1指向scope_1。
    2. 處理參數列表,在act_obj_1中設置屬性arg一、arg2,值分別爲10和20。建立arguments對象並進行設置,將arguments設置爲act_obj_1的屬性。
    3. 對outerFunc函數體進行語法分析,注意這裏在全局語法分析的時候並無對outerFunc函數體進行語法分析:
      1. 發現變量innerVar1,在act_obj_1中的varDecls添加innerVar1屬性,值爲undefined。
      2. < 發現函數innerFunc的定義,使用這個定義建立函數對象,並將scope_2傳遞給innerFunc,做爲innerFunc的[[scope]]屬性,在act_obj_1的funcDecls添加innerFunc。
    4. outerFunc函數語法分析結束,執行innerVar1賦值語句,賦值爲"var in function code"。
    5. 執行innerFunc,執行函數的處理流程是一致的:
      1. 建立調用對象,假設爲act_obj_2;同時將avt_obj_2連接起innerFunc[[scope]]屬性,構成一個新的做用域鏈,假設爲scope_3,scope_3中的第一個對象爲act_obj_2,act_obj_2指向scope_2。
      2. 處理參數列表,由於innerFunc沒有參數,因此只須要建立arguments對象並設置爲act_obj_2的屬性。
      3. 對innerFunc進行語法分析,識別變量和函數,但沒有發現變量定義和函數聲明。
      4. 執行innerFunc函數。對任何一個變量引用,從scope_3開始進行鏈式搜索,以scope_3->scope_2->scope_1的順序進行搜索,結果發現outerVar1在scope_1中的Global Object發現;innerVar一、arg一、arg2在scope_2中的act_obj_1中找到。
      5. 檢查scope_3和act_obj_2的引用,發現沒有其餘引用,則丟棄,讓引擎進行垃圾回收。
      6. 返回innerFunc執行的值。
    6. 檢查沒有對act_obj_1和scope_2的引用,則丟棄act_obj_1和scope_2。
    7. 返回結果。
  5. 將outerFunc的返回結果賦值給outerVar2。

咱們再拿前面代碼4的例子對做用域鏈進行簡單的分析:(精華2)
代碼4:

 1  function callMePlz() {
 2       var name = "stephenchan";
 3       var intro = function() {
 4           alert("I am " + name);
 5       }
 6       return intro;
 7   }
 8   
 9   function showMe(arg) {
10      var name = arg;
11      var func = callMePlz();
12      func();
13  }
14  showMe("endlesscode");

 

 假如全局的語法分析已經結束,已經開始執行"showMe('endlesscode')"了。在進入執行showMe的執行上下文時,咱們能夠看到"showMe"函數中[[scope]]屬性記錄中的做用域鏈爲:

1 [[scope]] = [
2     {//Global Object,由於在全局沒有var聲明的變量,所以就沒有列出來
3     document : ...,
4     location : ...
5     }
6 ]

建立showMe的調用對象後,則新的做用域鏈爲showMe的調用對象和全局調用對象組成:

 1  [scope chain] = [
 2     {//showme_activation_obj
 3         name : undefined,
 4         func : undefined,
 5         arg : "endlesscode",
 6         arguments : ...
 7      },
 8     {//Global Object,由於在全局沒有var聲明的變量,所以就沒有列出來
 9     document : ...,
10     location : ...
11     }
12 ]

也就是"showMe調用對象->Global調用對象"這樣的鏈式關係。接着,忽略showMe函數中語法分析等過程,到執行callMePlz()函數時,callMePlz函數的[[scope]]屬性爲

1 [[scope]] = [
2     {//Global Object,由於在全局沒有var聲明的變量,所以就沒有列出來
3     document : ...,
4     location : ...
5     }
6 ]

 

callMePlz函數的[[scope]]屬性指示的做用域鏈也只包括了全局調用對象,由於callMePlz也是在全局環境下定義的。建立callMePlz的調用對象後,則新的做用域鏈爲callMePlz的調用對象和全局調用對象組成:

 1  [scope chain] = [
 2     {//callmeplz_activation_obj
 3         name : undefined,
 4         intro : undefined,
 5         arguments : ...
 6      },
 7     {//Global Object,由於在全局沒有var聲明的變量,所以就沒有列出來
 8     document : ...,
 9     location : ...
10     }
11 ]

也就是"callMePlz調用對象->Global調用對象"這樣的鏈式關係。能夠看到,在callMePlz的做用域鏈中,並無包括showMe的調用對象。當callMePlz進行語法分析的時候,找到intro函數時,將intro函數的[[scope]]屬性賦值爲(上例分析中第2條中的第2條)

 1 [scope chain] = [
 2     {//callmeplz_activation_obj
 3         name : undefined,   //這裏仍是undefined,當語法分析結束,執行callMePlz時,這裏就賦值爲"stephenchan"
 4         intro : undefined,
 5         arguments : ...
 6      },
 7     {//Global Object,由於在全局沒有var聲明的變量,所以就沒有列出來
 8     document : ...,
 9     location : ...
10     }
11 ]

callMePlz返回的是intro函數對象的引用,當在showMe函數中執行intro函數時,建立intro函數的調用對象,此時intro函數的做用域鏈爲:

 1  [scope chain] = [
 2     {//intro_activation_obj
 3         arguments : ...
 4      },
 5     {//callmeplz_activation_obj
 6         name : undefined,   //這裏仍是undefined,當語法分析結束,執行callMePlz時,這裏就賦值爲"stephenchan"
 7         intro : undefined,
 8         arguments : ...
 9      },
10     {//Global Object,由於在全局沒有var聲明的變量,所以就沒有列出來
11     document : ...,
12     location : ...
13 
    }
14  ]

因爲在intro函數中沒有聲明變量和函數,因此看到的也只是一些內置的屬性成員,此時intro函數的做用域鏈則爲:"intro調用對象->callMePlz調用對象->Global調用對象",所以,當執行intro函數時,則以"intro調用對象->callMePlz調用對象->Global調用對象"的順序去搜索"name"變量,發如今callMePlz調用對象上找到了,所以在代碼4中輸出的是"I am stephenchan"而不是"I am endlesscode"。

以上面的分析方法來分析上述的其餘代碼,就容易理解其輸出了。

 

 

另外,函數閉包內的標識符系統有優先順序,其優先級從高到低爲:this > 局部變量(varDecls) > 函數形式參數名(argsName) > arguments關鍵字 > 函數名(funcNames)。

 1 //輸出'hi',說明argsName > funcNames。
 2 function foo(foo) {
 3     alert(foo);
 4 }
 5 foo('hi');
 6 
 7 //輸出100的類型"number",說明argsName > arguments。
 8 function foo2(arguments) {
 9     alert(typeof arguments);
10 }
11 foo2(100);
12 
13 //輸出arguments的類型爲'object‘,說明arguments > funcNames。
14 function arguments() {
15     alert(typeof arguments);
16 }
17 arguments();
18 
19 //輸出'test',形式參數名與未賦值局部變量重複時,取形式參數值。
20 function foo3(str) {
21     var str;
22     alert(str);
23 }
24 foo3('test');
25 
26 //輸出'member',形式參數與有值的局部變量重複時,取局部變量。
27 function foo4(str) {
28     var str = 'member';
29     alert(str);
30 }
31 foo4('test');

原文連接:http://blog.endlesscode.com/2010/01/20/javascript-closure-scope-chain/

推薦閱讀:http://www.cnblogs.com/lhb25/archive/2011/09/06/javascript-scope-chain.html

相關文章
相關標籤/搜索