1. 程式人生 > >javascript 作用域鏈

javascript 作用域鏈

問題的提出

首先看一個例子:

  1. var name = 'laruence' ;
  2. function echo () {
  3.      alert ( name);
  4.      var name = 'eve' ;
  5.      alert ( name);
  6.      alert ( age);
  7. }
  8. echo ();

執行結果是什麼呢?

上面的問題, 我相信會有很多人會認為是:

  1. laruence
  2. eve
  3. [指令碼出錯]

因為會以為在echo中, 第一次alert的時候, 會取到全域性變數name的值, 而第二次值被區域性變數name覆蓋, 所以第二次alert是’eve’. 而age屬性沒有定義, 所以指令碼會出錯.

但其實, 執行結果應該是:

  1. undefined
  2. eve
  3. [指令碼出錯]

為什麼呢?

JavaScript的作用域鏈

首先讓讓我們來看看Javasript(簡稱JS, 不完全代表JScript)的作用域的原理: JS權威指南中有一句很精闢的描述: ”JavaScript中的函式執行在它們被定義的作用域裡,而不是它們被執行的作用域裡.” 

為了接下來的知識, 你能順利理解, 我再提醒一下, 在JS中:”一切皆是物件, 函式也是”.

在JS中,作用域的概念和其他語言差不多, 在每次呼叫一個函式的時候 ,就會進入一個函式內的作用域,當從函式返回以後,就返回呼叫前的作用域.

JS的語法風格和C/C++類似, 但作用域的實現卻和C/C++不同,並非用“堆疊”方式,而是使用列表,具體過程如下(ECMA262中所述):
任何執行上下文時刻的作用域, 都是由作用域鏈(scope chain, 後面介紹)來實現.
在一個函式被定義的時候, 會將它定義時刻的scope chain連結到這個函式物件的[[scope]]屬性.
在一個函式物件被呼叫的時候,會建立一個活動物件(也就是一個物件), 然後對於每一個函式的形參,都命名為該活動物件的命名屬性, 然後將這個活動物件做為此時的作用域鏈(scope chain)最前端, 並將這個函式物件的[[scope]]加入到scope chain中.

看個例子:

  1.      var func = function ( lps, rps) {
  2.           var name = 'laruence' ;
  3.           ........
  4.      }
  5.      func ();

在執行func的定義語句的時候, 會建立一個這個函式物件的[[scope]]屬性(內部屬性,只有JS引擎可以訪問, 但FireFox的幾個引擎(SpiderMonkey和Rhino)提供了私有屬性__parent__來訪問它), 並將這個[[scope]]屬性, 連結到定義它的作用域鏈上(後面會詳細介紹), 此時因為func定義在全域性環境, 所以此時的[[scope]]只是指向全域性活動物件window active object.

在呼叫func的時候, 會建立一個活動物件(假設為aObj, 由JS引擎預編譯時刻建立, 後面會介紹),並建立arguments屬性, 然後會給這個物件新增倆個命名屬性aObj.lps, aObj.rps; 對於每一個在這個函式中申明的區域性變數和函式定義, 都作為該活動物件的同名命名屬性.

然後將呼叫引數賦值給形引數,對於缺少的呼叫引數,賦值為undefined。

然後將這個活動物件做為scope chain的最前端, 並將func的[[scope]]屬性所指向的,定義func時候的頂級活動物件, 加入到scope china.

有了上面的作用域鏈, 在發生識別符號解析的時候, 就會逆向查詢當前scope chain列表的每一個活動物件的屬性,如果找到同名的就返回。找不到,那就是這個識別符號沒有被定義。

注意到, 因為函式物件的[[scope]]屬性是在定義一個函式的時候決定的, 而非呼叫的時候, 所以如下面的例子:

  1.      var name = 'laruence' ;
  2.      function echo () {
  3.           alert ( name);
  4.      }
  5.      function env () {
  6.           var name = 'eve' ;
  7.           echo ();
  8.      }
  9.      env ();

執行結果是:

  1. laruence

結合上面的知識, 我們來看看下面這個例子:

  1. function factory () {
  2.      var name = 'laruence' ;
  3.      var intro = function () {
  4.           alert ( 'I am ' + name);
  5.      }
  6.      return intro;
  7. }
  8. function app ( para) {
  9.      var name = para;
  10.      var func = factory ();
  11.      func ();
  12. }
  13. app ( 'eve' );

當呼叫app的時候, scope chain是由: {window活動物件(全域性)}->{app的活動物件} 組成.

在剛進入app函式體時, app的活動物件有一個arguments屬性, 倆個值為undefined的屬性: name和func. 和一個值為’eve’的屬性para;

此時的scope chain如下:

  1. [[ scope chain]] = [
  2. {
  3.      para : 'eve' ,
  4.      name : undefined ,
  5.      func : undefined ,
  6.      arguments : []
  7. } , {
  8.      window call object
  9. }
  10. ]

當呼叫進入factory的函式體的時候, 此時的factory的scope chain為:

  1. [[ scope chain]] = [
  2. {
  3.      name : undefined ,
  4.      intor : undefined
  5. } , {
  6.      window call object
  7. }
  8. ]

注意到, 此時的作用域鏈中, 並不包含app的活動物件.

在定義intro函式的時候, intro函式的[[scope]]為:

  1. [[ scope chain]] = [
  2. {
  3.      name : 'laruence' ,
  4.      intor : undefined
  5. } , {
  6.      window call object
  7. }
  8. ]

從factory函式返回以後,在app體內呼叫intor的時候, 發生了識別符號解析, 而此時的sope chain是:

  1. [[ scope chain]] = [
  2. {
  3.      intro call object
  4. } , {
  5.      name : 'laruence' ,
  6.      intor : undefined
  7. } , {
  8.      window call object
  9. }
  10. ]

因為scope chain中,並不包含factory活動物件. 所以, name識別符號解析的結果應該是factory活動物件中的name屬性, 也就是’laruence’.

所以執行結果是:

  1. I am laruence

現在, 大家對”JavaScript中的函式執行在它們被定義的作用域裡,而不是它們被執行的作用域裡.”這句話, 應該有了個全面的認識了吧?

Javascript的預編譯

我們都知道,JS是一種指令碼語言, JS的執行過程, 是一種翻譯執行的過程.
那麼JS的執行中, 有沒有類似編譯的過程呢?

首先, 我們來看一個例子:

  1.      < script>
  2.      alert ( typeof eve); //function
  3.           function eve () {
  4.                alert ( 'I am Laruence' );
  5.           } ;
  6.      </ script>

誒? 在alert的時候, eve不是應該還是未定義的麼? 怎麼eve的型別還是function呢?

恩, 對, 在JS中, 是有預編譯的過程的, JS在執行每一段JS程式碼之前, 都會首先處理var關鍵字和function定義式(函式定義式和函式表示式).
如上文所說, 在呼叫函式執行之前, 會首先建立一個活動物件, 然後搜尋這個函式中的區域性變數定義,和函式定義, 將變數名和函式名都做為這個活動物件的同名屬性, 對於區域性變數定義,變數的值會在真正執行的時候才計算, 此時只是簡單的賦為undefined.

而對於函式的定義,是一個要注意的地方:

  1. < script>
  2.      alert ( typeof eve); //結果:function
  3.      alert ( typeof walle); //結果:undefined
  4.      function eve () { //函式定義式
  5.           alert ( 'I am Laruence' );
  6.      } ;
  7.      var walle = function () { //函式表示式
  8.      }
  9.      alert ( typeof walle); //結果:function
  10. </ script>

這就是函式定義式和函式表示式的不同, 對於函式定義式, 會將函式定義提前. 而函式表示式, 會在執行過程中才計算.

說到這裡, 順便說一個問題 :

  1.      var name = 'laruence' ;
  2.      age = 26 ;

我們都知道不使用var關鍵字定義的變數, 相當於是全域性變數, 聯絡到我們剛才的知識:

在對age做識別符號解析的時候, 因為是寫操作, 所以當找到到全域性的window活動物件的時候都沒有找到這個識別符號的時候, 會在window活動物件的基礎上, 返回一個值為undefined的age屬性.

也就是說, age會被定義在頂級作用域中.

現在, 也許你注意到了我剛才說的: JS在執行每一段JS程式碼 ..
對, 讓我們看看下面的例子:

  1. < script>
  2.      alert ( typeof eve); //結果:undefined
  3. </ script>
  4. < script>
  5.      function eve () {
  6.           alert ( 'I am Laruence' );
  7.      }
  8. </ script>

明白了麼? 也就是JS的預編譯是以段為處理單元的…

揭開謎底

現在讓我們回到我們的第一個問題:

當echo函式被呼叫的時候, echo的活動物件已經被預編譯過程建立, 此時echo的活動物件為:

  1. [ callObj] = {
  2. name : undefined
  3. }

當第一次alert的時候, 發生了識別符號解析, 在echo的活動物件中找到了name屬性, 所以這個name屬性, 完全的遮擋了全域性活動物件中的name屬性.

現在你明白了吧?

Related Posts: