閉包這東西,說難也難,說不難也不難,下面我就以自己的理解來說一下閉包
一、閉包的解釋說明
對於函數式語言來說,函數可以保存內部的數據狀態。對於像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中原生對象溝通不暢導致。