閉包是JavaScript中一個重要的特性,其最大的作用在於保存函數運行過程中的信息。在JavaScript中,閉包的諸多特性源自函數調用過程中的作用域鏈上。
函數調用對象與變量的作用域鏈
對於JavaScript中的每一次函數調用,JavaScript都會創建一個局部對象以儲存在該函數中定義的局部變量;如果在該函數內部還有一個嵌套定義的函數(nested function),那麼JavaScript會在已經定義的局部對象之上再定義一個嵌套局部對象。對於一個函數,其內部有多少層的嵌套函數定義,也就有多少層的嵌套局部對象。該局部對象稱為“函數調用對象”(ECMAScript 3中的“call object”,ECMAScript 5中改名為“declarative environment record”,但個人認為還是ECMAScript 3中的名稱更容易理解一些)。以下面的函數調用為例:
復制代碼 代碼如下:
function f(x){
var a = 10;
return a*x;
}
console.log(f(6));//60
在這個簡單的例子中,當調用f()函數時,JavaScript會創建一個f()函數的調用對象(姑且稱之為f_invokeObj),在f_invokeObj對象內部有兩個屬性:a和x;運行f()時,a值為10而x值為6,因此最後的返回結果為60。圖示如下:
當存在函數嵌套時,JavaScript將創建多個函數調用對象:
復制代碼 代碼如下:
function f(x){
var a = 10;
return a*g(x);
function g(b){
return b*b;
}
}
console.log(f(6));//360
在這個例子中,當調用f()函數時,JavaScript會創建一個f()函數的調用對象(f_invokeObj),其內部有兩個屬性a和x,a值為10而x值為6;運行f()時,JavaScript會對f()函數中的g()函數進行解析定義,並創建g()的調用對象(g_invokeObj),其內部有一個屬性b,b值與傳入參數x相同為6,因此最後的返回結果為360。圖示如下:
可以看到,函數調用對象形成了一條鏈。當內嵌函數g()運行,需要獲取變量值的時候,會從最近的函數調用對象中開始進行搜索,如果無法搜索到,則沿函數調用對象鏈在更遠的調用對象中進行搜尋,此即所謂的“變量的作用域鏈”。如果兩個函數調用對象中出現相同的變量,則函數會取離自己最近的那個調用對象中的變量值:
復制代碼 代碼如下:
function f(x){
var a = 10;
return a*g(x);
function g(b){
var a = 1;
return b*b*a;
}
}
console.log(f(6));//360, not 3600
在上面的例子中,g()函數的調用對象(g_invokeObj)和f()函數的調用對象(f_invokeObj)中均存在變量a且a的值不同,當運行g()函數時,在g()函數內部所使用的a值為1,而在g()函數外部所使用的a值則為10。圖示此時的函數調用對象鏈如下:
什麼是閉包?
在JavaScript中所有的函數(function)都是對象,而定義函數時都會產生相應的函數調用對象鏈,一次函數定義對應一個函數調用對象鏈。只要函數對象存在,相應的函數調用對象就存在;一旦某函數不再被使用,相應的函數調用對象就會被垃圾回收掉;而這種函數對象和函數調用對象鏈之間的一一組合,就稱之為“閉包”。在上面f()函數和g()函數的例子中,就存在兩個閉包:f()函數對象和f_invokeObj對象組成了一個閉包,而g()函數對象和g_invokeObj-f_invokeObj對象鏈一起組成了第二個閉包。當g()函數執行完畢後,由於g()函數不再被使用,因此g()閉包被垃圾回收了;之後,當f()函數執行完畢後,由於同樣的原因,f()閉包也被垃圾回收了。
從閉包的定義可以得出結論:所有的JavaScript函數在定義後都是閉包 – 因為所有的函數都是對象,所有的函數在執行後也都有其對應的調用對象鏈。
不過,令閉包真正發揮作用的是嵌套函數的情況。由於內嵌函數是在外部函數運行的時候才開始定義的,因此內嵌函數的閉包中所保存的變量值(尤其是外部函數的局部變量值)是這次運行過程中的值。只要內嵌函數對象依然存在,那麼其閉包就依然存在(閉包中的變量值不會發生任何改變),從而也就實現了保存函數運行過程的信息這個目的。考慮以下這個例子:
復制代碼 代碼如下:
var a = "outside";
function f(){
var a = "inside";
function g(){return a;}
return g;
}
var result = f();
console.log(result());//inside
在這個例子中,當運行f()函數時,g()函數被定義,同時創建了g()函數的閉包,g()閉包包含了g_invokeObj-f_invokeObj對象鏈,因此保存了f()函數執行過程中的變量a的值。當執行console.log()語句時,由於g函數對象仍然存在,因此g()閉包也依然存在;當運行這個仍然存在的g函數對象時,JavaScript會使用依然存在的g()閉包並從中獲取變量a的值(“inside”)。