【嵌套函數】
JavaScript允許嵌入的函數,允許函數用作資料,並且在函數詞法範圍下面,可以產生與傳統物件導向語言不同的驚人地方。
首先,JavaScript的函數是通過詞法來劃分範圍的,而不是動態劃分範圍的,於是,函數的是在定義它們的範圍中運行,而不是在執行它們的範圍中運行,所以,當嵌套函數和它的外圍函數定義在同一個詞法範圍中的時候,是很容易理解的。比如下面很平淡無奇的代碼:
複製代碼 代碼如下:var x = 'global';
function f () {
var x = 'local';
function g() {
alert(x);
}
g();
}
f(); // 'local'
當f()調用的時候,範圍鏈可以理解為由兩部分組成,包含f這一調用的調用對象,然後後面是全域對象。此時尋找x的值,會先從f的調用對象中尋找,如果沒有,再尋找後面全域對象中x。同理,g因為是f的一個嵌套函數,那麼,g調用的時候,範圍鏈應該就是由三部分組成了,g的調用對象,f的調用對象,和全域對象。函數g是要輸出x的值,所以會先在g的調用對象中尋找x的值,g中沒有定義,接下來尋找外圍f調用對象中x的定義,於是找到了x='local',那麼就會輸出x,而不會繼續往下尋找全域對象了。 如果f中也沒定義x的值,那麼就會繼續尋找範圍鏈後面的全域對象,結果就是global了。如果全域對象中也沒定義,那麼自然就是undefined。
好了,我們對範圍鏈有了個初步的理解,同時我們知道,閉包有兩個比較常用的用途,一個是可以利用它訪問到局部變數,另一個是可以把它外圍範圍中的變數值儲存在記憶體中而不在函數調用完畢後就銷毀。
下面接著看一個平淡無奇的例子,或許可以協助理解為什麼閉包可以把外部變數值儲存在記憶體中了。 複製代碼 代碼如下:function makeFunc (x) {
return function () {return x++}
}
var a = [makeFunc(0), makeFunc(1), makeFunc(2)];
alert(a[0]());
alert(a[1]());
alert(a[2]());
執行結果為0,1,2 ;也沒有什麼特別的地方,這也是嚴格的詞法範圍的正常表現。每次makeFunc調用完畢後,它的調用對象會從範圍鏈中移除,再沒有任何對它的引用,最終通過垃圾收集而完結。說的詳細一點,我們可以這樣理解。
makeFunc每次調用的時候,會為他建立一個調用對象放置到範圍鏈中。針對makeFunc這個函數而言,這個調用對象包含一個屬性x(也就是函數的參數,因為函數參數可以看做調用對象的一個屬性),makeFunc會返回一個匿名嵌套函數的引用,接下來這個匿名嵌套函數執行,又會建立一個調用對象,放置到範圍鏈中,匿名函數返回x的值,(注意:匿名函數的調用對象中是沒有x的定義的,於是它會引用到它外圍的函數makeFunc的調用對象,訪問到x)然後x加1,至此,匿名函數執行完畢,它調用對象從範圍鏈中移除, 然後makeFunc也執行完畢,makeFunc調用對象也被移除。由於它的調用對象中包含x,所以x也隨著它的銷毀而銷毀。不會儲存下來。
以上就是函數的詳細的執行過程,請仔細理解後看看下面改動的代碼: 複製代碼 代碼如下:var x = 0;
function makeFunc () {
return function () {return x++}
}
var a = [makeFunc(), makeFunc(), makeFunc()];
alert(a[0]());
alert(a[1]());
alert(a[2]());
現在x是一個全域變數了,執行結果為0,1,2;但是這個結果就與上面的有些不同了。下面我們還是從範圍鏈的方向來理解這個結果產生的原因。
同樣,makeFunc每次調用的時候會建立一個調用對象到範圍鏈中,由於它返回內部嵌套函數的引用,所以內部嵌套函數開始執行,又建立一個嵌套函數的調用對象到範圍鏈。然後返回x的值,注意,這裡就不同了,嵌套函數的調用對象中沒有x,它外圍的makeFunc的調用對象中也沒有x,只能接著往下尋找到全域對象中,在全域對象中找到了x的定義,於是正常執行,返回x的值,x加1,然後嵌套函數完畢,調用對象移除,接著makeFunc完畢,調用對象也移除,可是因為他們的調用對象中都沒有x,他們的調用對象銷毀根本不會影響到x。於是,全域變數x值的改變就這樣被儲存下來了。
注意,上面說的訪問外圍的調用對象只是為了協助理解而不嚴格的說法,JavaScript不會以任何方式直接存取調用對象,但是,它定義的屬性作為調用對象中範圍鏈的一部分,還是“活的”。另外,如果一個外圍函數包含了兩個或多個嵌套函數都對全域對象有引用,那麼這些嵌套函數都共用同一個全域調用對象,並且其中一個對全域對象的改變對其他的都是可見的。
好了,在JavaScript裡,函數是將要執行的代碼以及執行這些代碼的範圍構成的一個綜合體,廣義的說,我們就可以把這種代碼和範圍的綜合體叫做閉包。
【閉包】
我們偶爾需要寫一個需要通過調用來記住一個變數值的函數。於是,如果我們瞭解了範圍,就會知道,局部變數是很難做到的,因為函數的調用對象不能在調用後一直維持。全域變數可以做到,就如上面的例子一樣,但是這樣很容易造成全域變數汙染。既然調用對象不能維持,那麼我們不把值儲存在調用對象中不就行了?!所以,下面是實現的一種方法:用函數對象自身的屬性來儲存。 複製代碼 代碼如下:uniqueID = function () {
if (!arguments.callee.id) arguments.callee.id = 0;
return arguments.callee.id ++;
}
alert(uniqueID()); //0
alert(uniqueID()); //1
如上,因為函數本身就是一個對象,所以,我們用它自身的一個屬性來儲存是可行的,但是這樣做有一個問題,就是任何人在任何時候都可以通過unqueID.id強制訪問到我們原本儲存到的值並作出修改。這是我們不願看到的。
所以,通常,我們使用閉包來實現這件事。如下: 複製代碼 代碼如下:_uniqueID = (function(){
var id = 0;
return function () {return id ++}
})();
alert(_uniqueID()); //0
alert(_uniqueID()); //1
同樣,我們也用作用鏈域來解釋下結果。注意到_uniqueID本身就是一個匿名函數,它內部又有個匿名嵌套函數,我們直接調用的是_uniqueID(),也就是說,我們直接調用的其實是_uniqueID內部的嵌套函數,而它本身的調用對象沒有定義id,於是引用外圍的調用對象中的id,並返回,id加1,執行完畢,內層嵌套函數調用對象移出範圍鏈。而外圍的id並沒有被銷毀,於是就這樣儲存了下來。
有人可能會疑惑,不是說調用對象在函數執行完畢後就移除了範圍鏈嗎,外圍匿名函數(function(){})();也是調用完畢了的,應該調用對象也沒了才對。
是的,調用對象是在當前函數執行完畢後就結束引用,但是這裡不要誤解了上面_uniqueID()的調用,他並不是直接調用的外圍函數,而是調用的嵌套函數,嵌套函數的範圍鏈是包含外圍函數的範圍鏈的。所以在它的調用對象移除範圍鏈的時候是能夠訪問到這條範圍鏈上其他對象的屬性並改變的。
閉包本身就是個難以理解但是又非常有用的東西,希望能對有需要的人一些協助吧。此外,資曆所限,本人理解也可能有誤,如發現,敬請指正。