什么是閉包
- 了解什么是閉包之前,要先了解變量的作用域,變量的作用域無非就是兩種:全局變量和局部變量。
Javascript語言的特殊之處,就在于函數內部可以直接讀取全局變量。
var n=999;
function f1(){
alert(n);
}
f1(); // 999
另一方面,在函數外部自然無法讀取函數內的局部變量。
function f1(){
var n=999;
}
alert(n); // error
這里有一個地方需要注意,函數內部聲明變量的時候,一定要使用var命令。如果不用的話,你實際上聲明了一個全局變量!
function f1(){
n=999;
}
f1();
alert(n); // 999
- 如何從外部讀取局部變量
出于種種原因,我們有時候需要得到函數內的局部變量。但是,前面已經說過了,正常情況下,這是辦不到的,只有通過變通方法才能實現。
那就是在函數的內部,再定義一個函數。
function f1(){
var n=999;
function f2(){ alert(n); // 999 }
}
在上面的代碼中,函數f2就被包括在函數f1內部,這時f1內部的所有局部變量,對f2都是可見的。但是反過來就不行,f2內部的局部變量,對f1就是不可見的。這就是Javascript語言特有的"鏈式作用域"結構(chain scope),子對象會一級一級地向上尋找所有父對象的變量。所以,父對象的所有變量,對子對象都是可見的,反之則不成立。
既然f2可以讀取f1中的局部變量,那么只要把f2作為返回值,我們不就可以在f1外部讀取它的內部變量了嗎!
function f1(){
var n=999;
function f2(){ alert(n); }
return f2;
}
var result=f1();
result(); // 999
上一節代碼中的f2函數,就是閉包。
各種專業文獻上的"閉包"(closure)定義非常抽象,很難看懂。我的理解是,閉包就是能夠讀取其他函數內部變量的函數。
由于在Javascript語言中,只有函數內部的子函數才能讀取局部變量,因此可以把閉包簡單理解成"定義在一個函數內部的函數"。
所以,在本質上,閉包就是將函數內部和函數外部連接起來的一座橋梁。
- 我們來看這么一段代碼
function add(){
var i = 0;
return function(){
alert(i++);
}
}
var f = add();
f();
f();
首先我們從作用域的角度來描述它,我們先定義一個全局的作用域,然后我們看add的詞法環境。
當我們執行add的時候,它實際上返回了一個函數,這個函數對象在JS里面怎么描述?
我們知道,當一個函數創建的時候,在引擎內部會創建一個函數對象,這個函數里面會包含形參,函數體的內容以及會保存當前的應用環境。
也就是說,當我們返回的這個函數,它會保存當前add的作用域環境,即它的scope會指向add environment。
當我們執行f函數的時候,首先也會創建這個函數的詞法作用域,我們暫且成為closure environment。同時,我們也會執行i++的操作。此時我們需要尋找變量i,實際上就在它的外層(outer)的作用域里面,也就說,當我們執行i++的時候,它順著作用域鏈找到了add environment里面的i,找到之后給它加了1。
同理,繼續執行f后,i就變成2。
一般來說,函數執行完以后,內部的局部變量會被釋放掉,等同于不存在了,但是JS里面比較特殊,它允許函數作為返回值,此時函數有可能引用外部變量,這時它就保存了對外部詞法環境的引用,這就是閉包的特性。
- 總結如下
- 閉包由函數和其相關的引用環境的組合而成
- 閉包允許函數訪問其引用環境中的變量(又稱為自由變量)
- 廣義上來說,所有JS函數都可以稱為閉包,因為JS
- 函數在創建時保存了當前的詞法環境
閉包的應用-保存變量現場
-
我們首先來看這段代碼
這段代碼是用來做事件注冊,在addHandeler里面呢,它會為一組元素去注冊onclick事件,當onclick觸發的時候,它可以把元素索引打出來,即希望當我點到第零個元素的時候,alert出來0,第一個元素的時候,alert出來1。
我們還是從作用域開始,我們看到onclick這里的i其實是addHandler里面的i,不管循環體執行多少次,onclick產生的函數訪問的i都是最外層的i,也就是說,當一個函數改變里面i的值,i的值就會跟這邊,那么最后我們去訪問i的時候,i的值就已經變了,因為這個循環在不斷的對i++,最后我們onclick的時候得到的i值,會變成node.length而不是01234。 -
我們再來看這段代碼
這里我們把node[i].onclick賦給了helper函數,helper返回了一個函數,可以看出helper是閉包的作用,i是形參,可以被內部函數訪問,也就是可以被里面的函數所訪問,helper都會產生閉包保存當前的值,當我們執行函數的時候,就會彈出當時傳進去的值,從而到達保存變量現場的目的
閉包的應用-封裝
-
做信息隱藏,有些變量我們不能暴露給外部,或者說,我們通過自執行函數+閉包方法減少一些不必要的全局變量,但是同時又能夠使這些變量在各自的自執行函數內有效。
- 這里我們observer函數里面返回了一組屬性,這些屬性都是接口,正常來說我們是通過外部是訪問不到observer里面的observerList這個變量的,然而我們可以通過這些接口來訪問observerList這個變量
使用閉包的注意點
- 由于閉包會使得函數中的變量都被保存在內存中,內存消耗很大,所以不能濫用閉包,否則會造成網頁的性能問題,在IE中可能導致內存泄露。解決方法是,在退出函數之前,將不使用的局部變量全部刪除。
- 閉包會在父函數外部,改變父函數內部變量的值。所以,如果你把父函數當作對象(object)使用,把閉包當作它的公用方法(Public Method),把內部變量當作它的私有屬性(private value),這時一定要小心,不要隨便改變父函數內部變量的值。
思考題
如果你能理解下面兩段代碼的運行結果,應該就算理解閉包的運行機制了。
- 代碼片段一
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
return function(){
return this.name;
};
}
};
alert(object.getNameFunc()());
- 代碼片段二
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
var that = this;
return function(){
return that.name;
};
}
};
alert(object.getNameFunc()());