1. 程式人生 > >JavaScript中的閉包

JavaScript中的閉包

search 賦值 狀態 調用 value 標識符 mas 直接 art

  有不少開發人員總是搞不清匿名函數和閉包這兩個概念,因此經常混用。閉包是指有權訪問另一個函數作用域中的變量的函數。創建閉包的常見方式,就是在一個函數內部創建另一個函數。看下面的例子

 1 function createComparisonFunction(propertyName) {
 2   return function(object1, object2) {
 3     var value1 = object1[propertyName];
 4     var value2 = object2[propertyName];
 5     if (value1 < value2) {
6 return -1; 7 } else if (value1 > value2) { 8 return 1; 9 } else { 10 return 0; 11 } 12 } 13 } 14 15 var clo = createComparisonFunction(‘name‘); 16 var res = clo({‘name‘:‘Jeff‘}, {‘name‘:‘Tim‘}); 17 console.log(res);

  至於inner function為什麽能夠訪問parent function中的變量(這裏指的是propertyName)?

  我們需要先來搞清楚幾個概念,執行環境(execution context),活動對象(activation object),變量對象(variable object),作用域鏈(scope chain)

  什麽是執行環境?可以抽象的理解成是一個Object,它有一系列的屬性,也叫做當前執行環境的狀態。看下面的圖表,除了這三個必要的屬性,一個執行環境也可能會有其它的屬性,這取決於它的實現。  

  技術分享圖片

  什麽是變量對象?變量對象是和當前執行環境相關聯的一個數據容器,它是一個特殊的對象,存儲著當前執行環境中的變量和函數聲明,註意,不包括函數表達式,在不同的執行環境中,它指代的對象也不同,例如,在全局環境中,變量對象就是全局對象本身,這也是為什麽我們能夠通過全局對象(window)的屬性引用全局變量了,那在function context中,變量對象又是什麽呢?在函數作用域中,變量對象可認為是活動對象(activation object)

  那什麽是活動對象呢?當一個函數被調用的時候活動對象會被創建,調用這個函數的主體我們稱之為caller,caller也是一個特殊的對象,活動對象包含函數的參數,特殊的arguments對象(一個函數參數和索引的map),還有函數內部的變量和函數聲明。在函數作用域中,活動對象被作為是當前環境中的變量對象使用。看下面的例子

1 function foo(x, y) {
2   var z = 30;
3   function bar() {} // FD
4   (function baz() {}); // FE
5 }
6  
7 foo(10, 20);
  foo function context的活動對象是這樣的   技術分享圖片

  什麽是作用域鏈呢?A scope chain is a list of objects that are searched for identifiers appear in the code of the context.規則類似於prototype chain,如果某個變量標識符在當前作用域中沒有找到,則到parent的作用域(變量對象)中去查找,一層層往上查找,這樣在當前環境中沒有查找到需要到上層才能查到的變量稱之為free variable。而free variable的查找則是通過作用域鏈實現的,通常來說,一個作用域鏈就是一系列的parent variable objects 加上 函數自身的變量對象或活動對象,然而,這個作用域鏈也可能包含一些在環境執行期間被動態添加的對象,例如使用with 聲明 或 catch。

  來看下面的例子

 1 function compare(value1, value2) {
 2   if (value1 < value2) {
 3     return -1;
 4   } else if (value1 > value2) {
 5     return 1;
 6   } {
 7     return 0;
 8   }
 9 }
10 
11 var result = compare(5, 10);

  下圖展示了包含上述關系的compare函數執行時的作用域鏈

  技術分享圖片      

  全局環境的變量對象始終存在,而像compare()函數這樣的局部環境的變量對象,則只在函數執行的過程中存在。註意這些對象的先後順序,在創建compare函數時,會創建一個預先包含全局變量對象的作用域鏈,這個作用域鏈被保存在內部的[[Scope]]屬性中。當調用compare函數時,會為函數創建一個執行環境,然後通過復制函數的[[Scope]]屬性中的對象構建起執行環境的作用域鏈。此後,又有一個活動對象(在此作為變量對象使用)被創建並被推入執行環境作用域鏈的前端。對於這個例子中compare函數的執行環境而言,其作用域鏈中包含兩個變量對象,本地活動對象和全局變量對象。顯然,作用域鏈本質上是一個指向變量對象的指針列表,它只引用但不實際包含變量對象。

  下面回到最初的那個閉包函數,看一下它的作用域鏈

技術分享圖片

  在一個函數內部定義的函數會將包含函數(即外部函數)的活動對象添加到它的作用域鏈中。因此在createComparisonFunction內定義的匿名函數的作用域鏈中,實際上會包含外部函數createComparisonFunction的活動對象。在匿名函數被返回後,它的作用域鏈被初始化為包含createComparisonFunction的活動對象和全局變量對象,這樣匿名函數就可以訪問在createComparisonFunction中定義的變量了。更為重要的是,createComparisonFunction函數在執行完畢後,其活動對象也不會被銷毀,因為匿名函數的作用域鏈仍然在引用這個活動對象,換句話說,createComparisonFunction函數返回後,其執行環境的作用域鏈會被銷毀,但它的活動對象仍然會留在內存中;直到匿名函數被銷毀,createComparisonFunction的活動對象才會被銷毀,這裏執行代碼clo = null即可銷毀匿名函數。

  由於閉包會攜帶包含它的函數的作用域,因此會比其它函數占用更多的內存。過度使用閉包可能會導致內存占用過多。

  下面我們來看閉包與變量的兩個例子

 1 function createFunctions() {
 2   var result = new Array();
 3   
 4   for (var i = 0; i < 10; i++) {
 5     result[i] = function() {
 6       return i;
 7     }
 8   }
 9   return result;
10 }
11 
12 var result = createFunctions();
13 console.log(result[1]());    // 10
14 console.log(result[3]());   // 10
15 
16 function createFunctionsChanged() {
17   var result = new Array();
18   
19   for (var i = 0; i < 10; i++) {
20     result[i] = function(num) {
21       return function(){
22         return num;
23       };
24     }(i)
25   }
26   return result;
27 }
28 
29 var result = createFunctionsChanged();
30 console.log(result[1]());   // 1
31 console.log(result[3]());  // 3

  在第二個例子中,我們沒有直接把閉包賦值給數組,而是定義了一個匿名函數,並將立即執行匿名函數的結果賦值給數組。 在調用每個匿名函數時,我們傳入了變量i。由於函數傳值是按值傳遞的,所以就會將變量i的當前值復制給參數num。而在匿名函數的內部,又創建並返回了一個訪問num的閉包。這樣一來,result數組中的每個函數都有自己的num變量的一個副本,也就可以返回不同的數值了。

  更多詳細解釋請參考

  • http://dmitrysoshnikov.com/ecmascript/javascript-the-core/

JavaScript中的閉包