閉包向來給包括JavaScript程序員在內(nèi)的程序員以神秘,高深的感覺帆焕,事實上坡疼,閉包的概念在函數(shù)式編程語言中算不上是難以理解的知識。如果對作用域灵妨,函數(shù)為獨立的對象這樣的基本概念理解較好的話解阅,理解閉包的概念并在實際的編程實踐中應用則頗有水到渠成之感。
在DOM的事件處理方面泌霍,大多數(shù)程序員甚至自己已經(jīng)在使用閉包了而不自知货抄,在這種情況下,對于瀏覽器中內(nèi)嵌的JavaScript引擎的bug可能造成內(nèi)存泄漏這一問題姑且不論朱转,就是程序員自己調(diào)試也常常會一頭霧水蟹地。
用簡單的語句來描述JavaScript中的閉包的概念:由于JavaScript中,函數(shù)是對象藤为,對象是屬性的集合怪与,而屬性的值又可以是對象,則在函數(shù)內(nèi)定義函數(shù)成為理所當然缅疟,如果在函數(shù)func內(nèi)部聲明函數(shù)inner分别,然后在函數(shù)外部調(diào)用inner,這個過程即產(chǎn)生了一個閉包存淫。
閉包的特性
我們先來看一個例子茎杂,如果不了解JavaScript的特性,很難找到原因:
var outter = [];
function clouseTest () {
var array = ["one", "two", "three", "four"];
for(var i = 0; i < array.length;i++){
var x = {};
x.no = i;
x.text = array[i];
x.invoke = function(){
print(i);
}
outter.push(x);
}
}
//調(diào)用這個函數(shù)
clouseTest();
print(outter[0].invoke());
print(outter[1].invoke());
print(outter[2].invoke());
print(outter[3].invoke());
運行的結果如何呢纫雁?很多初學者可能會得出這樣的答案:
0
1
2
3
然而煌往,運行這個程序,得到的結果為:
4
4
4
4
其實轧邪,在每次迭代的時候刽脖,這樣的語句x.invoke = function(){print(i);}并沒有被執(zhí)行,只是構建了一個函數(shù)體為”print(i);”的函數(shù)對象忌愚,如此而已曲管。而當i=4時,迭代停止硕糊,外部函數(shù)返回院水,當再去調(diào)用outter[0].invoke()時,i的值依舊為4简十,因此outter數(shù)組中的每一個元素的invoke都返回i的值:4檬某。
如何解決這一問題呢?我們可以聲明一個匿名函數(shù)螟蝙,并立即執(zhí)行它:
var outter = [];
function clouseTest2(){
var array = ["one", "two", "three", "four"];
for(var i = 0; i < array.length;i++){
var x = {};
x.no = i;
x.text = array[i];
x.invoke = function(no){
return function(){
print(no);
}
}(i);
outter.push(x);
}
}
clouseTest2();
這個例子中恢恼,我們?yōu)閤.invoke賦值的時候,先運行一個可以返回一個函數(shù)的函數(shù)胰默,然后立即執(zhí)行之场斑,這樣漓踢,x.invoke的每一次迭代器時相當與執(zhí)行這樣的語句:
//x == 0
x.invoke = function(){print(0);}
//x == 1
x.invoke = function(){print(1);}
//x == 2
x.invoke = function(){print(2);}
//x == 3
x.invoke = function(){print(3);}
這樣就可以得到正確結果了。閉包允許你引用存在于外部函數(shù)中的變量漏隐。然而喧半,它并不是使用該變量創(chuàng)建時的值,相反青责,它使用外部函數(shù)中該變量最后的值挺据。
閉包的用途
現(xiàn)在,閉包的概念已經(jīng)清晰了爽柒,我們來看看閉包的用途吴菠。事實上者填,通過使用閉包浩村,我們可以做很多事情。比如模擬面向對象的代碼風格占哟;更優(yōu)雅心墅,更簡潔的表達出代碼;在某些方面提升代碼的執(zhí)行效率榨乎。
匿名自執(zhí)行函數(shù)
上一節(jié)中的例子怎燥,事實上就是閉包的一種用途,根據(jù)前面講到的內(nèi)容可知蜜暑,所有的變量铐姚,如果不加上var關鍵字,則默認的會添加到全局對象的屬性上去肛捍,這樣的臨時變量加入全局對象有很多壞處隐绵,比如:別的函數(shù)可能誤用這些變量;造成全局對象過于龐大拙毫,影響訪問速度(因為變量的取值是需要從原型鏈上遍歷的)依许。除了每次使用變量都是用var關鍵字外,我們在實際情況下經(jīng)常遇到這樣一種情況缀蹄,即有的函數(shù)只需要執(zhí)行一次峭跳,其內(nèi)部變量無需維護,比如UI的初始化缺前,那么我們可以使用閉包:
var datamodel = {
table : [],
tree : {}
};
(function(dm){
for(var i = 0; i < dm.table.rows; i++){
var row = dm.table.rows[i];
for(var j = 0; j < row.cells; i++){
drawCell(i, j);
}
}
//build dm.tree
})(datamodel);
我們創(chuàng)建了一個匿名的函數(shù)蛀醉,并立即執(zhí)行它,由于外部無法引用它內(nèi)部的變量衅码,因此在執(zhí)行完后很快就會被釋放滞欠,關鍵是這種機制不會污染全局對象。
緩存
再來看一個例子肆良,設想我們有一個處理過程很耗時的函數(shù)對象筛璧,每次調(diào)用都會花費很長時間逸绎,那么我們就需要將計算出來的值存儲起來,當調(diào)用這個函數(shù)的時候夭谤,首先在緩存中查找棺牧,如果找不到,則進行計算朗儒,然后更新緩存并返回值颊乘,如果找到了,直接返回查找到的值即可醉锄。閉包正是可以做到這一點乏悄,因為它不會釋放外部的引用,從而函數(shù)內(nèi)部的值可以得以保留恳不。
var CachedSearchBox = (function(){
var cache = {},
count = [];
return {
attachSearchBox : function(dsid){
if(dsid in cache){//如果結果在緩存中
return cache[dsid];//直接返回緩存中的對象
}
var fsb = new uikit.webctrl.SearchBox(dsid);//新建
cache[dsid] = fsb;//更新緩存
if(count.length > 100){//保正緩存的大小<=100
delete cache[count.shift()];
}
return fsb;
},
clearSearchBox : function(dsid){
if(dsid in cache){
cache[dsid].clearSelection();
}
}
};
})();
CachedSearchBox.attachSearchBox("input1");
這樣檩小,當我們第二次調(diào)用CachedSearchBox.attachSerachBox(“input1”)的時候,我們就可以從緩存中取道該對象烟勋,而不用再去創(chuàng)建一個新的searchbox對象规求。
實現(xiàn)封裝
可以先來看一個關于封裝的例子,在person之外的地方無法訪問其內(nèi)部的變量卵惦,而通過提供閉包的形式來訪問:
<strong>var person = function(){
//變量作用域為函數(shù)內(nèi)部阻肿,外部無法訪問
var name = "default";
return {
getName : function(){
return name;
},
setName : function(newName){
name = newName;
}
}
}();
print(person.name);//直接訪問,結果為undefined
print(person.getName());
person.setName("abruzzi");
print(person.getName());</strong>
得到結果如下:
undefined
default
abruzzi
閉包的另一個重要用途是實現(xiàn)面向對象中的對象沮尿,傳統(tǒng)的對象語言都提供類的模板機制丛塌,這樣不同的對象(類的實例)擁有獨立的成員及狀態(tài),互不干涉畜疾。雖然JavaScript中沒有類這樣的機制赴邻,但是通過使用閉包,我們可以模擬出這樣的機制庸疾。還是以上邊的例子來講:
function Person(){
var name = "default";
return {
getName : function(){
return name;
},
setName : function(newName){
name = newName;
}
}
};
var john = Person();
print(john.getName());
john.setName("john");
print(john.getName());
var jack = Person();
print(jack.getName());
jack.setName("jack");
print(jack.getName());
運行結果如下:
default
john
default
jack
由此代碼可知乍楚,john和jack都可以稱為是Person這個類的實例,因為這兩個實例對name這個成員的訪問是獨立的届慈,互不影響的徒溪。
事實上,在函數(shù)式的程序設計中金顿,會大量的用到閉包臊泌,我們將在第八章討論函數(shù)式編程,在那里我們會再次探討閉包的作用揍拆。
應該注意的問題
內(nèi)存泄漏
在不同的JavaScript解釋器實現(xiàn)中渠概,由于解釋器本身的缺陷,使用閉包可能造成內(nèi)存泄漏,內(nèi)存泄漏是比較嚴重的問題播揪,會嚴重影響瀏覽器的響應速度贮喧,降低用戶體驗,甚至會造成瀏覽器無響應等現(xiàn)象猪狈。
JavaScript的解釋器都具備垃圾回收機制箱沦,一般采用的是引用計數(shù)的形式,如果一個對象的引用計數(shù)為零雇庙,則垃圾回收機制會將其回收谓形,這個過程是自動的。但是疆前,有了閉包的概念之后寒跳,這個過程就變得復雜起來了,在閉包中竹椒,因為局部的變量可能在將來的某些時刻需要被使用童太,因此垃圾回收機制不會處理這些被外部引用到的局部變量,而如果出現(xiàn)循環(huán)引用碾牌,即對象A引用B康愤,B引用C儡循,而C又引用到A舶吗,這樣的情況使得垃圾回收機制得出其引用計數(shù)不為零的結論,從而造成內(nèi)存泄漏择膝。
上下文的引用
關于this我們之前已經(jīng)做過討論誓琼,它表示對調(diào)用對象的引用,而在閉包中肴捉,最容易出現(xiàn)錯誤的地方是誤用了this腹侣。在前端JavaScript開發(fā)中,一個常見的錯誤是錯將this類比為其他的外部局部變量:
$(function(){
var con = $("div#panel");
this.id = "content";
con.click(function(){
alert(this.id);//panel
});
});
此處的alert(this.id)到底引用著什么值呢齿穗?很多開發(fā)者可能會根據(jù)閉包的概念傲隶,做出錯誤的判斷:
content
理由是,this.id顯示的被賦值為content,而在click回調(diào)中窃页,形成的閉包會引用到this.id跺株,因此返回值為content。然而事實上脖卖,這個alert會彈出”panel”乒省,究其原因,就是此處的this,雖然閉包可以引用局部變量畦木,但是涉及到this的時候袖扛,情況就有些微妙了,因為調(diào)用對象的存在十籍,使得當閉包被調(diào)用時(當這個panel的click事件發(fā)生時)蛆封,此處的this引用的是con這個jQuery對象唇礁。而匿名函數(shù)中的this.id = “content”是對匿名函數(shù)本身做的操作。兩個this引用的并非同一個對象惨篱。
如果想要在事件處理函數(shù)中訪問這個值垒迂,我們必須做一些改變:
$(function(){
var con = $("div#panel");
this.id = "content";
var self = this;
con.click(function(){
alert(self.id);//content
});
});
這樣,我們在事件處理函數(shù)中保存的是外部的一個局部變量self的引用妒蛇,而并非this机断。這種技巧在實際應用中多有應用,我們在后邊的章節(jié)里進行詳細討論绣夺。關于閉包的更多內(nèi)容吏奸,我們將在第九章詳細討論,包括討論其他命令式語言中的“閉包”陶耍,閉包在實際項目中的應用等等奋蔚。