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