1. 程式人生 > >JavaScript關於作用域、作用域鏈和閉包的理解

JavaScript關於作用域、作用域鏈和閉包的理解

作用域

先來談談變數的作用域  變數的作用域無非就是兩種:全域性變數和區域性變數。  全域性作用域:  最外層函式定義的變數擁有全域性作用域,即對任何內部函式來說,都是可以訪問的:

<script>
      var outerVar = "outer";
      function fn(){
         console.log(outerVar);
      }
      fn();//result:outer
   </script>

區域性作用域:  和全域性作用域相反,區域性作用域一般只在固定的程式碼片段內可訪問到,而對於函式外部是無法訪問的,最常見的例如函式內部

<script>
      function fn(){
         var innerVar = "inner";
      }
      fn();
      console.log(innerVar);// ReferenceError: innerVar is not defined
</script>

需要注意的是,函式內部宣告變數的時候,一定要使用var命令。如果不用的話,你實際上聲明瞭一個全域性變數!

 <script>
      function fn(){
         innerVar = "inner";
      }
      fn();
      console.log(innerVar);// result:inner
   </script>

再來看一個程式碼:

<script>
      var scope = "global";
      function fn(){
         console.log(scope);//result:undefined
         var scope = "local";
         console.log(scope);//result:local;
      }
      fn();
   </script>

很有趣吧,第一個輸出居然是undefined,原本以為它會訪問外部的全域性變數(scope=”global”),但是並沒有。這可以算是javascript的一個特點,只要函式內定義了一個區域性變數,函式在解析的時候都會將這個變數“提前宣告”:

   <script>
      var scope = "global";
      function fn(){
         var scope;//提前聲明瞭區域性變數
         console.log(scope);//result:undefined
         scope = "local";
         console.log(scope);//result:local;
      }
      fn();
   </script>

然而,也不能因此草率地將區域性作用域定義為:用var宣告的變數作用範圍起止於花括號之間。  javascript並沒有塊級作用域  那什麼是塊級作用域?  像在C/C++中,花括號內中的每一段程式碼都具有各自的作用域,而且變數在宣告它們的程式碼段之外是不可見的,比如下面的c語言程式碼:

for(int i = 0; i < 10; i++){
//i的作用範圍只在這個for迴圈
}
printf("%d",&i);//error

但是javascript不同,並沒有所謂的塊級作用域,javascript的作用域是相對函式而言的,可以稱為函式作用域:

<script>
      for(var i = 1; i < 10; i++){
            //coding
      }
      console.log(i); //10  
   </script>

作用域鏈(Scope Chain)

那什麼是作用域鏈?  我的理解就是,根據在內部函式可以訪問外部函式變數的這種機制,用鏈式查詢決定哪些資料能被內部函式訪問。  想要知道js怎麼鏈式查詢,就得先了解js的執行環境

執行環境(execution context) 每個函式執行時都會產生一個執行環境,而這個執行環境怎麼表示呢?js為每一個執行環境關聯了一個變數物件。環境中定義的所有變數和函式都儲存在這個物件中。  全域性執行環境是最外圍的執行環境,全域性執行環境被認為是window物件,因此所有的全域性變數和函式都作為window物件的屬性和方法建立的。  js的執行順序是根據函式的呼叫來決定的,當一個函式被呼叫時,該函式環境的變數物件就被壓入一個環境棧中。而在函式執行之後,棧將該函式的變數物件彈出,把控制權交給之前的執行環境變數物件。  舉個例子:

   <script>
      var scope = "global"; 
      function fn1(){
         return scope; 
      }
      function fn2(){
         return scope;
      }
      fn1();
      fn2();
   </script>

上面程式碼執行情況演示: 

瞭解了環境變數,再詳細講講作用域鏈。  當某個函式第一次被呼叫時,就會建立一個執行環境(execution context)以及相應的作用域鏈,並把作用域鏈賦值給一個特殊的內部屬性([scope])。然後使用this,arguments(arguments在全域性環境中不存在)和其他命名引數的值來初始化函式的活動物件(activation object)。當前執行環境的變數物件始終在作用域鏈的第0位。  以上面的程式碼為例,當第一次呼叫fn1()時的作用域鏈如下圖所示:  (因為fn2()還沒有被呼叫,所以沒有fn2的執行環境) 

ä½ç¨åé¾

可以看到fn1活動物件裡並沒有scope變數,於是沿著作用域鏈(scope chain)向後尋找,結果在全域性變數物件裡找到了scope,所以就返回全域性變數物件裡的scope值。

識別符號解析是沿著作用域鏈一級一級地搜尋識別符號地過程。搜尋過程始終從作用域鏈地前端開始,然後逐級向後回溯,直到找到識別符號為止(如果找不到識別符號,通常會導致錯誤發生)—-《JavaScript高階程式設計》

那作用域鏈地作用僅僅只是為了搜尋識別符號嗎?  再來看一段程式碼:

   <script>
      function outer(){
         var scope = "outer";
         function inner(){
            return scope;
         }
         return inner;
      }
      var fn = outer();
      fn();
   </script>

outer()內部返回了一個inner函式,當呼叫outer時,inner函式的作用域鏈就已經被初始化了(複製父函式的作用域鏈,再在前端插入自己的活動物件),具體如下圖: 

一般來說,當某個環境中的所有程式碼執行完畢後,該環境被銷燬(彈出環境棧),儲存在其中的所有變數和函式也隨之銷燬(全域性執行環境變數直到應用程式退出,如網頁關閉才會被銷燬)  但是像上面那種有內部函式的又有所不同,當outer()函式執行結束,執行環境被銷燬,但是其關聯的活動物件並沒有隨之銷燬,而是一直存在於記憶體中,因為該活動物件被其內部函式的作用域鏈所引用。  具體如下圖:  outer執行結束,內部函式開始被呼叫  outer執行環境等待被回收,outer的作用域鏈對全域性變數物件和outer的活動物件引用都斷了 

é­åä½ç¨åé¾ 像上面這種內部函式的作用域鏈仍然保持著對父函式活動物件的引用,就是閉包(closure)

閉包

閉包有兩個作用:  第一個就是可以讀取自身函式外部的變數(沿著作用域鏈尋找)  第二個就是讓這些外部變數始終儲存在記憶體中  關於第二點,來看一下以下的程式碼:

   <script>
      function outer(){
         var result = new Array();
         for(var i = 0; i < 2; i++){//注:i是outer()的區域性變數
            result[i] = function(){
               return i;
            }
         }
         return result;//返回一個函式物件陣列
         //這個時候會初始化result.length個關於內部函式的作用域鏈
      }
      var fn = outer();
      console.log(fn[0]());//result:2
      console.log(fn[1]());//result:2
   </script>

返回結果很出乎意料吧,你肯定以為依次返回0,1,但事實並非如此  來看一下呼叫fn[0]()的作用域鏈圖: 

é­åä½ç¨åé¾

可以看到result[0]函式的活動物件裡並沒有定義i這個變數,於是沿著作用域鏈去找i變數,結果在父函式outer的活動物件裡找到變數i(值為2),而這個變數i是父函式執行結束後將最終值儲存在記憶體裡的結果。  由此也可以得出,js函式內的變數值不是在編譯的時候就確定的,而是等在執行時期再去尋找的。

那怎麼才能讓result陣列函式返回我們所期望的值呢?  看一下result的活動物件裡有一個arguments,arguments物件是一個引數的集合,是用來儲存物件的。  那麼我們就可以把i當成引數傳進去,這樣一呼叫函式生成的活動物件內的arguments就有當前i的副本。  改進之後:

 <script>
      function outer(){
         var result = new Array();
         for(var i = 0; i < 2; i++){
            //定義一個帶參函式
            function arg(num){
               return num;
            }
            //把i當成引數傳進去
            result[i] = arg(i);
         }
         return result;
      }
      var fn = outer();
      console.log(fn[0]);//result:0
      console.log(fn[1]);//result:1
   </script>

雖然的到了期望的結果,但是又有人問這算閉包嗎?呼叫內部函式的時候,父函式的環境變數還沒被銷燬呢,而且result返回的是一個整型陣列,而不是一個函式陣列!  確實如此,那就讓arg(num)函式內部再定義一個內部函式就好了:  這樣result返回的其實是innerarg()函式

   <script>
      function outer(){
         var result = new Array();
         for(var i = 0; i < 2; i++){
            //定義一個帶參函式
            function arg(num){
               function innerarg(){
                  return num;
               }
               return innerarg;
            }
            //把i當成引數傳進去
            result[i] = arg(i);
         }
         return result;
      }
      var fn = outer();
      console.log(fn[0]());
      console.log(fn[1]());
   </script>

當呼叫outer,for迴圈內i=0時的作用域鏈圖如下: 

é­åä½ç¨åé¾

由上圖可知,當呼叫innerarg()時,它會沿作用域鏈找到父函式arg()活動物件裡的arguments引數num=0.  上面程式碼中,函式arg在outer函式內預先被呼叫執行了,對於這種方法,js有一種簡潔的寫法

    function outer(){
         var result = new Array();
         for(var i = 0; i < 2; i++){
            //定義一個帶參函式
            result[i] = function(num){
               function innerarg(){
                  return num;
               }
               return innerarg;
            }(i);//預先執行函式寫法
            //把i當成引數傳進去
         }
         return result;
      }
1

關於this物件

關於閉包經常會看到這麼一道題:

var name = "The Window";
  var object = {
    name : "My Object",
    getNameFunc : function(){
      return function(){
        return this.name;
      };
    }
  };
  alert(object.getNameFunc()());//result:The Window

《javascript高階程式設計》一書給出的解釋是:

this物件是在執行時基於函式的執行環境繫結的:在全域性函式中,this等於window,而當函式被作為某個物件呼叫時,this等於那個物件。不過,匿名函式具有全域性性,因此this物件同常指向window

參考資料