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