閉包

閉包向來給包括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)容吏奸,我們將在第九章詳細討論,包括討論其他命令式語言中的“閉包”陶耍,閉包在實際項目中的應用等等奋蔚。

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市烈钞,隨后出現(xiàn)的幾起案子泊碑,更是在濱河造成了極大的恐慌,老刑警劉巖毯欣,帶你破解...
    沈念sama閱讀 222,104評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件馒过,死亡現(xiàn)場離奇詭異,居然都是意外死亡酗钞,警方通過查閱死者的電腦和手機腹忽,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來砚作,“玉大人窘奏,你說我怎么就攤上這事『迹” “怎么了着裹?”我有些...
    開封第一講書人閱讀 168,697評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長米同。 經(jīng)常有香客問我骇扇,道長,這世上最難降的妖魔是什么窍霞? 我笑而不...
    開封第一講書人閱讀 59,836評論 1 298
  • 正文 為了忘掉前任匠题,我火速辦了婚禮,結果婚禮上但金,老公的妹妹穿的比我還像新娘韭山。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 68,851評論 6 397
  • 文/花漫 我一把揭開白布钱磅。 她就那樣靜靜地躺著梦裂,像睡著了一般。 火紅的嫁衣襯著肌膚如雪盖淡。 梳的紋絲不亂的頭發(fā)上年柠,一...
    開封第一講書人閱讀 52,441評論 1 310
  • 那天,我揣著相機與錄音褪迟,去河邊找鬼冗恨。 笑死,一個胖子當著我的面吹牛味赃,可吹牛的內(nèi)容都是我干的掀抹。 我是一名探鬼主播,決...
    沈念sama閱讀 40,992評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼心俗,長吁一口氣:“原來是場噩夢啊……” “哼傲武!你這毒婦竟也來了?” 一聲冷哼從身側響起城榛,我...
    開封第一講書人閱讀 39,899評論 0 276
  • 序言:老撾萬榮一對情侶失蹤揪利,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后狠持,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體疟位,經(jīng)...
    沈念sama閱讀 46,457評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,529評論 3 341
  • 正文 我和宋清朗相戀三年工坊,在試婚紗的時候發(fā)現(xiàn)自己被綠了献汗。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片敢订。...
    茶點故事閱讀 40,664評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡王污,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出楚午,到底是詐尸還是另有隱情昭齐,我是刑警寧澤,帶...
    沈念sama閱讀 36,346評論 5 350
  • 正文 年R本政府宣布矾柜,位于F島的核電站阱驾,受9級特大地震影響,放射性物質發(fā)生泄漏怪蔑。R本人自食惡果不足惜里覆,卻給世界環(huán)境...
    茶點故事閱讀 42,025評論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望缆瓣。 院中可真熱鬧喧枷,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,511評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至戚扳,卻和暖如春忧便,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背帽借。 一陣腳步聲響...
    開封第一講書人閱讀 33,611評論 1 272
  • 我被黑心中介騙來泰國打工珠增, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人砍艾。 一個月前我還...
    沈念sama閱讀 49,081評論 3 377
  • 正文 我出身青樓切平,卻偏偏與公主長得像,于是被迫代替她去往敵國和親辐董。 傳聞我的和親對象是個殘疾皇子悴品,可洞房花燭夜當晚...
    茶點故事閱讀 45,675評論 2 359

推薦閱讀更多精彩內(nèi)容