閉包這東西,說難也難,說不難也不難,下面我就以自己的理解來說一下閉包
一、閉包的解釋說明
對於函數式語言來說,函數可以儲存內部的資料狀態。對於像C#這種編譯型命令式語言來說,由於代碼總是在程式碼片段中執行,而程式碼片段是唯讀,因此函數中的資料只能是待用資料。函數內部的局部變數存放在棧上,在函數執行結束以後,所佔用的棧被釋放,因此局部變數是不能儲存的。
Javascript採用詞法範圍,函數的執行依賴於變數範圍,這個範圍是在定義函數時確定的。因此Javascript中函數對象不僅儲存代碼邏輯,還必須引用當前的範圍鏈。Javascript中函數內部的局部變數可以被修改,而且當再次進入到函數內部的時候,上次被修改的狀態仍然持續。這是因為因為局部變數並不儲存在棧上,而是通過一個對象來儲存。
決定使用哪個變數是由範圍鏈決定的,每次產生函數執行個體時,都會為之建立一個對象用來儲存局部變數,並且把這個用於儲存局部變數的對象加入範圍鏈中。不同函數對象可以通過範圍鏈關聯起來。Javascript中所有函數都是閉包,我們不能避免“產生”閉包。
引用一張《Javascript進階程式設計》中的圖來說明,雖然這張圖並不完全說明所有情況。圖中的activation object就是用於儲存變數的對象。
簡而言之,在Javascript中:
閉包:函數執行個體儲存著在執行時所需要的變數的引用,而不會複製儲存當時變數的值。(在Object C的實現中,我們可以選擇儲存當時的值或者是引用)
範圍鏈:解析變數時尋找變數所在的方式,以var作為終止符號,如果鏈上一直沒有var,則一直追溯到全域對象為止。
C#中的閉包特性是由編譯器把局部變數轉換成參考型別的對象成員實現的。
二、閉包的使用
下面通過一些具體例子來說明如何利用閉包這一特性:
1.閉包是在定義的時候產生的
function Foo(){ function A(){} function B(){} function C(){}}
我們每次執行Foo()的時候,都有有A,B,C這三個函數執行個體(閉包)產生,當Foo執行完畢,產生的執行個體沒有其他引用,因此會被當成垃圾隨之銷毀(不一定是馬上銷毀)。
我們來證實一下範圍鏈是在函數定義時確定的,所以這裡顯示的應該是'local scope'
var scope = "global scope"; function checkscope() { var scope = "local scope"; function f() { return scope; } return f;}checkscope()()
同樣道理:
(function(){ function A(){} function B(){} function C(){}}())
上面的運算式執行完後也會有A,B,C這三個函數執行個體(閉包)產生,因為這是一個立即執行的匿名函數,這三個閉包只能產生一次。產生的閉包沒有其他引用,因此會被當成垃圾隨之銷毀(不一定是馬上銷毀)。
我們之所以這麼寫,目地有兩個
1.避免汙染全域對象
2.避免多次產生相同的函數執行個體
對比下面兩個例子,閉包是如何儲存範圍鏈的:
function A(){} //比較省記憶體的寫法,建立對象速度快,開銷小 (function(prototype){ var name = "a"; function sayName () { alert(name); } function ChangeName() { name += "_changed" } prototype.sayName = sayName;//引用通過執行匿名函數產生的閉包,閉包只會產生一次 prototype.changeName = ChangeName; }(A.prototype)) var a1 = new A(); var a2 = new A();
a1.sayName(); a1.changeName(); a2.sayName();
--------------------------------------------------------------------------------
function B(){ //原型鏈比較短的做法,找到方法的速度快,但是比較耗記憶體,每次new 調用構造器都有2個函數執行個體和1個變數產生。 var name = "b"; function sayName () { alert(name); } function changeName() { name += "_changed"; } this.sayName = sayName;//引用閉包,每次調用函數B都會產生新的閉包 this.changeName = changeName; }//如果函數調用之前帶有new關鍵字,則函數作為構造器使用。//本質上來說作為構造器和作為普通函數調用沒區別。如果直接調用B(),那麼this對象會綁定到全域對象,新產生的閉包會代替舊的閉包賦給全域對象的changeName和sayName屬性上,因此舊的閉包會被當成記憶體回收。//如果作為構造器使用,new 關鍵字會產生一個新的對象(this指向這個新對象)並初始化這個新對象的sayName和changeName屬性,因此每次產生的閉包都會因為有引用而保留下來。 var b1 = new B(); b1.sayName(); b1.changeName(); b1.sayName(); var b2 = new B(); b2.sayName(); b1.sayName();
三、泄漏問題:在編譯語言中,函數體總在檔案的程式碼片段中,並在運行期被裝入標誌為可執行檔記憶體區。事實上我們不認為函數自身會有生命週期。我們在大多數情況下會認為“參考型別的資料結構”具有生存周期和泄漏的問題,如指標、對象等。
JavaScript中記憶體的泄漏本質上就是定義函數時產生的儲存局部變數的對象因為存在引用而不被當成垃圾被回收。
1.存在循環參考
2.有些對象總不能銷毀,如IE6在DOM中的記憶體流失,或者在銷毀時不能通知到Javascript引擎,因此也就有些Javascript閉包總不能被銷毀。這些情況通常是發生在Javascript宿主對象和Javascript中原生對象溝通不暢導致。