一擦盾、什么是閉包?
“官方”的解釋是:所謂“閉包”写半,指的是一個(gè)擁有許多變量和綁定了這些變量的環(huán)境的表達(dá)式(通常是一個(gè)函數(shù))又碌,因而這些變量也是該表達(dá)式的一部分九昧。
相信很少有人能直接看懂這句話绊袋,因?yàn)樗枋龅奶珜W(xué)術(shù)。我想用如何在Javascript中創(chuàng)建一個(gè)閉包來告訴你什么是閉包铸鹰,因?yàn)樘^閉包的創(chuàng)建過程直接理解閉包的定義是非常困難的癌别。看下面這段代碼:
function a(){
var i=0;
function b(){
alert(++i);
}
return b;
}
var c = a();
c();
這段代碼有兩個(gè)特點(diǎn):
1蹋笼、函數(shù)b嵌套在函數(shù)a內(nèi)部展姐;
2、函數(shù)a返回函數(shù)b剖毯。
這樣在執(zhí)行完var c=a()后圾笨,變量c實(shí)際上是指向了函數(shù)b,再執(zhí)行c()后就會(huì)彈出一個(gè)窗口顯示i的值(第一次為1)逊谋。這段代碼其實(shí)就創(chuàng)建了一個(gè)閉包擂达,為什么?因?yàn)楹瘮?shù)a外的變量c引用了函數(shù)a內(nèi)的函數(shù)b胶滋,就是說:
當(dāng)函數(shù)a的內(nèi)部函數(shù)b被函數(shù)a外的一個(gè)變量引用的時(shí)候板鬓,就創(chuàng)建了一個(gè)閉包。
我猜想你一定還是不理解閉包究恤,因?yàn)槟悴恢篱]包有什么作用俭令,下面讓我們繼續(xù)探索。
二丁溅、閉包有什么作用唤蔗?
簡而言之探遵,閉包的作用就是在a執(zhí)行完并返回后窟赏,閉包使得Javascript的垃圾回收機(jī)制GC不會(huì)收回a所占用的資源,因?yàn)閍的內(nèi)部函數(shù)b的執(zhí)行需要依賴a中的變量箱季。這是對閉包作用的非常直白的描述涯穷,不專業(yè)也不嚴(yán)謹(jǐn),但大概意思就是這樣藏雏,理解閉包需要循序漸進(jìn)的過程拷况。
在上面的例子中,由于閉包的存在使得函數(shù)a返回后掘殴,a中的i始終存在赚瘦,這樣每次執(zhí)行c(),i都是自加1后alert出i的值奏寨。
那 么我們來想象另一種情況起意,如果a返回的不是函數(shù)b,情況就完全不同了病瞳。因?yàn)閍執(zhí)行完后揽咕,b沒有被返回給a的外界悲酷,只是被a所引用,而此時(shí)a也只會(huì)被b引 用亲善,因此函數(shù)a和b互相引用但又不被外界打擾(被外界引用)设易,函數(shù)a和b就會(huì)被GC回收。(關(guān)于Javascript的垃圾回收機(jī)制將在后面詳細(xì)介紹)
三蛹头、閉包內(nèi)的微觀世界
如 果要更加深入的了解閉包以及函數(shù)a和嵌套函數(shù)b的關(guān)系顿肺,我們需要引入另外幾個(gè)概念:函數(shù)的執(zhí)行環(huán)境(excution context)、活動(dòng)對象(call object)掘而、作用域(scope)挟冠、作用域鏈(scope chain)。以函數(shù)a從定義到執(zhí)行的過程為例闡述這幾個(gè)概念袍睡。
1知染、當(dāng)定義函數(shù)a的時(shí)候,js解釋器會(huì)將函數(shù)a的作用域鏈(scope chain)設(shè)置為定義a時(shí)a所在的“環(huán)境”斑胜,如果a是一個(gè)全局函數(shù)控淡,則scope chain中只有window對象。
2止潘、當(dāng)函數(shù)a執(zhí)行的時(shí)候掺炭,a會(huì)進(jìn)入相應(yīng)的執(zhí)行環(huán)境(excution context)。
3凭戴、在創(chuàng)建執(zhí)行環(huán)境的過程中涧狮,首先會(huì)為a添加一個(gè)scope屬性,即a的作用域么夫,其值就為第1步中的scope chain者冤。即a.scope=a的作用域鏈。
4档痪、然后執(zhí)行環(huán)境會(huì)創(chuàng)建一個(gè)活動(dòng)對象(call object)涉枫。活動(dòng)對象也是一個(gè)擁有屬性的對象腐螟,但它不具有原型而且不能通過JavaScript代碼直接訪問愿汰。創(chuàng)建完活動(dòng)對象后,把活動(dòng)對象添加到a的作用域鏈的最頂端乐纸。此時(shí)a的作用域鏈包含了兩個(gè)對象:a的活動(dòng)對象和window對象衬廷。
5、下一步是在活動(dòng)對象上添加一個(gè)arguments屬性汽绢,它保存著調(diào)用函數(shù)a時(shí)所傳遞的參數(shù)吗跋。
6、最后把所有函數(shù)a的形參和內(nèi)部的函數(shù)b的引用也添加到a的活動(dòng)對象上庶喜。在這一步中小腊,完成了函數(shù)b的的定義救鲤,因此如同第3步,函數(shù)b的作用域鏈被設(shè)置為b所被定義的環(huán)境秩冈,即a的作用域本缠。
到此,整個(gè)函數(shù)a從定義到執(zhí)行的步驟就完成了入问。此時(shí)a返回函數(shù)b的引用給c丹锹,又函數(shù)b的作用域鏈包含了對函數(shù)a的活動(dòng)對象的引用,也就是說b可以訪問到a中定義的所有變量和函數(shù)芬失。函數(shù)b被c引用楣黍,函數(shù)b又依賴函數(shù)a,因此函數(shù)a在返回后不會(huì)被GC回收棱烂。
當(dāng)函數(shù)b執(zhí)行的時(shí)候亦會(huì)像以上步驟一樣租漂。因此,執(zhí)行時(shí)b的作用域鏈包含了3個(gè)對象:b的活動(dòng)對象颊糜、a的活動(dòng)對象和window對象哩治,如下圖所示:
如圖所示,當(dāng)在函數(shù)b中訪問一個(gè)變量的時(shí)候衬鱼,搜索順序是先搜索自身的活動(dòng)對象业筏,如果存在則返回,如果不存在將繼續(xù)搜索函數(shù)a的活動(dòng)對象鸟赫,依 次查找蒜胖,直到找到為止。如果整個(gè)作用域鏈上都無法找到抛蚤,則返回undefined台谢。如果函數(shù)b存在prototype原型對象,則在查找完自身的活動(dòng)對象 后先查找自身的原型對象霉颠,再繼續(xù)查找对碌。這就是Javascript中的變量查找機(jī)制荆虱。
四蒿偎、閉包的應(yīng)用場景
1、保護(hù)函數(shù)內(nèi)的變量安全怀读。以最開始的例子為例诉位,函數(shù)a中i只有函數(shù)b才能訪問,而無法通過其他途徑訪問到菜枷,因此保護(hù)了i的安全性苍糠。
2、在內(nèi)存中維持一個(gè)變量啤誊。依然如前例岳瞭,由于閉包拥娄,函數(shù)a中i的一直存在于內(nèi)存中,因此每次執(zhí)行c()瞳筏,都會(huì)給i自加1稚瘾。
以上兩點(diǎn)是閉包最基本的應(yīng)用場景,很多經(jīng)典案例都源于此姚炕。
五摊欠、閉包的優(yōu)缺點(diǎn)
優(yōu)點(diǎn):?
1.保護(hù)函數(shù)內(nèi)的變量安全,加強(qiáng)了封裝性?
2.在內(nèi)存中維持一個(gè)變量(用的太多就變成了缺點(diǎn),占內(nèi)存)?
閉包之所以會(huì)占用資源是當(dāng)函數(shù)a執(zhí)行結(jié)束后, 變量i不會(huì)因?yàn)楹瘮?shù)a的結(jié)束而銷毀, 因?yàn)閎的執(zhí)行需要依賴a中的變量柱宦。
缺點(diǎn):
閉包有一個(gè)非常嚴(yán)重的問題些椒,那就是內(nèi)存浪費(fèi)問題,這個(gè)內(nèi)存浪費(fèi)不僅僅因?yàn)樗qv內(nèi)存掸刊,更重要的是免糕,對閉包的使用不當(dāng)會(huì)造成無效內(nèi)存的產(chǎn)生
五、Javascript的垃圾回收機(jī)制
在Javascript中忧侧,如果一個(gè)對象不再被引用说墨,那么這個(gè)對象就會(huì)被GC回收。如果兩個(gè)對象互相引用苍柏,而不再被第3者所引用尼斧,那么這兩個(gè)互相引用的對象也會(huì)被回收。因?yàn)楹瘮?shù)a被b引用试吁,b又被a外的c引用棺棵,這就是為什么函數(shù)a執(zhí)行后不會(huì)被回收的原因。