1、變量作用域
要理解閉包,首先要理解javascript的特殊的變量作用域。
變量的作用域無(wú)非就兩種:全局變量和局部變量。
javascript語(yǔ)言的特別之處就在于:函數(shù)內(nèi)部可以直接讀取全局變量,但是在函數(shù)外部無(wú)法讀取函數(shù)內(nèi)部的局部變量。
注意點(diǎn):在函數(shù)內(nèi)部聲明變量的時(shí)候,一定要使用var命令。如果不用的話(huà),你實(shí)際上聲明的是一個(gè)全局變量!
function foo(){
? ? varx = 1;
? ? returnfunction (){
? ? ? ? alert(++x);//2? ? }
}varbar = foo();
bar();
先問(wèn)一個(gè)問(wèn)題,這里到底誰(shuí)是閉包?是foo還是那個(gè)匿名函數(shù)?
閉包的產(chǎn)生原理
在JavaScript中,函數(shù)可以用來(lái)分隔作用域,當(dāng)foo執(zhí)行(activation)的時(shí)候,產(chǎn)生了一個(gè)foo的動(dòng)態(tài)作用域,然后這個(gè)動(dòng)態(tài)作用域把變量x和那個(gè)return的匿名函數(shù)裝(push到棧)了進(jìn)去,一般情況下,當(dāng)函數(shù)執(zhí)行完畢時(shí),它會(huì)自動(dòng)銷(xiāo)毀(pop出棧)內(nèi)部產(chǎn)生的變量和函數(shù),跳出這個(gè)作用域環(huán)境,返回到上一層(context)。但是在這里,由于foo作用域內(nèi)部的變量和函數(shù)與它作用域外部的變量bar存在曖昧關(guān)系(bar引用了foo()的返回值),所以變量x和匿名函數(shù)沒(méi)法從foo作用域中被銷(xiāo)毀,于是也就產(chǎn)生了我們平時(shí)所說(shuō)的閉包。剛才說(shuō)的push到棧和pop出棧很已經(jīng)顯然不適用于閉包,這和棧的結(jié)構(gòu)是相悖的,那么閉包是怎樣的內(nèi)存分配方式呢?這個(gè)我們后面再說(shuō)。閉包既不是foo函數(shù),也不是那個(gè)匿名函數(shù),而是變量x、匿名函數(shù)、上下文環(huán)境三者一起同時(shí)存在的結(jié)果。
閉包存在有這么兩個(gè)條件:
沒(méi)有被創(chuàng)建它的上下文銷(xiāo)毀
引用了自由變量(沒(méi)有在函數(shù)塊中定義,也沒(méi)有從arguments中送入,如上匿名函數(shù)中的變量x,就是一個(gè)自由變量)
說(shuō)了這么多,再看看下面這個(gè)例子:
varx = 1;function foo(){
? ? alert(x);
}
~function(){
? ? varx = 2;
? ? foo(); //1
}();
你可能又不解了,這里怎么會(huì)彈出1呢?先說(shuō)明下,下面三種寫(xiě)法效果是等價(jià)的(但解析方式并不一樣,A、C是一類(lèi),B是另一類(lèi),這里就不多說(shuō)了):
~function(){
? ? varx = 2;
? ? foo();? ?
}();//A
(function(){
? ? varx = 2;
? ? foo();? ?
}());//B
(function(){
? ? varx = 2;
? ? foo();?
? })();//C
閉包的內(nèi)存分配方式
回歸正題,上面為什么會(huì)彈出1,這個(gè)閉包的情況和上面所述的閉包有些不太相同,上面的閉包是因?yàn)樽饔糜蛑械臇|西沒(méi)有被銷(xiāo)毀,并與上下文存在曖昧關(guān)系,而這里并不存在銷(xiāo)毀什么的問(wèn)題,但是它依舊是一個(gè)閉包。在foo中,x是一個(gè)自由變量,當(dāng)foo這個(gè)閉包產(chǎn)生的時(shí)候,foo的上下文會(huì)被保存,而foo處于Activation狀態(tài)的時(shí)候,它會(huì)先從他所處的Activation
Object(foo內(nèi)部聲明的變量、函數(shù)等非自由變量)中查找需要的對(duì)象,如果沒(méi)有找到,便會(huì)從它開(kāi)始保存的上下文中查找對(duì)象,如果還沒(méi)找到,才會(huì)跑到他的上一層作用域鏈中取那個(gè)值為2的x。
再回到之前說(shuō)的那個(gè)問(wèn)題,閉包的內(nèi)存分配方式。很明顯,如果閉包的內(nèi)存分配是利用棧的結(jié)構(gòu)實(shí)現(xiàn)的,那進(jìn)入foo運(yùn)行狀態(tài)的時(shí)候,應(yīng)該會(huì)push一個(gè)“全局“的x,也就是向上找到那個(gè)var x =2,接著alert(2);但事實(shí)并非如此,上層作用域的閉包數(shù)據(jù)是動(dòng)態(tài)分配的內(nèi)存,也就是保存在堆里,解析器會(huì)記錄這個(gè)閉包數(shù)據(jù)被引用的次數(shù),當(dāng)引用次數(shù)為0的時(shí)候,垃圾回收機(jī)制(GC)會(huì)自動(dòng)處理這些垃圾。
閉包是如何霸占內(nèi)存的
IE經(jīng)常會(huì)因?yàn)殚]包的存在而導(dǎo)致內(nèi)存居高不下。第一個(gè)例子中:
window<=>foo<=>匿名函數(shù)<=>bar<=>window
形成了一個(gè)引用循環(huán),即便是
bar = null;
這個(gè)匿名函數(shù)的引用次數(shù)依舊大于0。需要注意的即便是是delete一個(gè)變量并不是刪除這個(gè)變量的引用對(duì)象,而是斷開(kāi)這個(gè)引用,其作用就是讓引用對(duì)象的引用次數(shù)減1. 這樣一來(lái),這個(gè)閉包就死在內(nèi)存里了,于是它也就一直占用著內(nèi)存= =