標籤:cat 前端 catch 關係 過多 val array bar span
有不少開發人員總是搞不清匿名函數和閉包這兩個概念,因此經常混用。閉包是指有權訪問另一個函數範圍中的變數的函數。建立閉包的常見方式,就是在一個函數內部建立另一個函數。看下面的例子
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() {} // FD4 (function baz() {}); // FE5 }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]()); // 1014 console.log(result[3]()); // 1015 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]()); // 131 console.log(result[3]()); // 3
在第二個例子中,我們沒有直接把閉包賦值給數組,而是定義了一個匿名函數,並將立即執行匿名函數的結果賦值給數組。 在調用每個匿名函數時,我們傳入了變數i。由於函數傳值是按值傳遞的,所以就會將變數i的當前值複製給參數num。而在匿名函數的內部,又建立並返回了一個訪問num的閉包。這樣一來,result數組中的每個函數都有自己的num變數的一個副本,也就可以返回不同的數值了。
更多詳細解釋請參考
- http://dmitrysoshnikov.com/ecmascript/javascript-the-core/
JavaScript中的閉包