本文不談閉包的概念,因爲概念容易把人搞暈,本文希望通過幾個鮮活的例子來探究閉包的性質,相信對理解閉包會有所幫助。
程序1
var f = (function() { var n = 10; return function() { ++n; console.log(n); } })(); f();
輸出:
11
結論:
閉包函數可以訪問外層函數中的變量。
程序2
var arr = []; (function() { var n = 0; for (var i=0; i<3; ++i) { arr[i] = function() { ++n; console.log(n); } } })(); for (var i=0; i<3; ++i) { var f = arr[i]; f(); }
輸出:
1 2 3
結論:
一個函數內有多個閉包函數,那麼這些閉包函數共享外層函數中的變量。可以看出,例子中3個閉包函數中的n是同一個變量,而不是該變量的3個副本。
程序3
var f0 = function() { ++n; console.log(n); } var f = (function() { var n = 10; return f0; })(); f();
輸出:
錯誤指向“++n”這一行。
結論:
閉包函數的作用域不是在引用或運行它的時候確定的,看起來好像是在定義它的時候確定的。
說明:
雖然該程序與“程序1”看起來一樣,但由於函數f0一個在內部定義一個在外部定義,儘管它們都是從函數內部返回,但在這個例子中f0卻無法訪問變量n。
程序4
var f = (function() { var n = 10; return new Function('++n;console.log(n);'); })(); f();
輸出同“程序3”:
結論:
該程序是對“程序3”的進一步印證和補充。由該程序可以得出的結論是:閉包函數的作用域是在編譯它的時候確定的。
說明:
使用Function構造器可以創建一個function對象,函數體使用一個字符串指定,它可以像普通函數一樣執行,並在首次執行時編譯。因此,雖然在匿名函數內部定義了此function對象,但一直要到調用它的時候纔會編譯,即執行到“f()”時,而此時原函數的作用域已經不存在了。
再看一個例子:
程序5
var f = (function() { var n = 10; return eval('(function(){++n;console.log(n);})'); })(); f();
輸出:
11
結論:無
說明:
這個例子是對上面兩個程序的補充。這個例子之所以能夠和“程序1”一樣打印出11,是因爲eval( )函數會立即對傳遞給它字符串進行解析(編譯、執行),因此使用eval定義的函數和直接定義的效果是等價的。
(注意:eval( )中的“function(){...}”必須用括號擴起來,否則會報錯)
程序6
var f = (function() { var n = 10; var s = 'hello'; return function() { ++n; console.log(n); } })(); f();
運行時在“console.log(n);”這一行加個斷點,查看作用域中的值,其中只有n沒有s:
結論:
外層函數中那些未在閉包函數中使用的變量,對閉包函數是不可見的。在這個例子中,閉包函數沒有引用過變量s,因此其作用域中只有n。也就是說,對閉包函數來說,其可以訪問的外層函數的變量實際上只是真正的外層函數變量的一個子集。
程序7
這個程序用來通過數據證明“程序6”的結論。程序稍微有點複雜,後面會先對其做簡單說明。
var funArr = []; var LENGTH = 500; var ALPHABET = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; function getStr( ) { var s = ''; for (var i=0; i<LENGTH; ++i) { s += ALPHABET[Math.floor(Math.random( ) * 62)]; } return s; } function getArr( ) { var a = new Array(LENGTH); for (var i=0; i<LENGTH; ++i) { a[i] = Math.random( ); } } var f = function( ) { var n = 10; var s = getStr( ); var a = getArr( ); funArr.push(function( ) { console.log(n, s, a); // --- 1 console.log(n); // --- 2 }) } for (var i=0; i<2000; ++i) { f( ); }
程序分析:
本程序的重點是for循環和函數f。for循環中調用了函數f 2000次,每次調用都會創建一個數字和兩個長度爲500的字符串和數組,所以2000次函數調用所創建的局部變量的規模還是比較可觀的,程序用這種方法以便於後面做分析時對結果進行比較。
f中的局部變量會被一個閉包函數所引用,以此觀察未被引用的局部變量是否會被回收。
分別運行該程序兩次,第一次使用語句1(引用了f中的所有局部變量),第二次使用語句2(只引用了數字變量n)。對運行所得到的結果1和結果2分別採集堆快照(Heap Snapshot):
可以看到所佔內存差別巨大,從這裏就可以初步得出“未被閉包函數引用的局部變量會被回收”的結論。
不過爲了嚴謹性,需要做更細緻地分析。首先是結果1和結果2的統計圖:
可以看到,第二次運行後內存中的對象數量明顯要比第一次的少很多(二者產生的中間對象數量是相同的),這進一步說明了第二次運行後大部分對象都被回收了。
最後我們將結果2與結果1進行細緻的比較,結果如下:
結論:
函數中的局部變量如果沒有被任何閉包函數所引用(這裏不考慮被全局變量所引用的情況),則這些局部變量在函數運行完成後就可以被回收,否則,這些變量會作爲其閉包函數的作用域的一部分被保留,直到所有閉包函數也執行完畢爲止。
該結論同時也印證了“程序6”的結論:閉包函數對外層函數作用域的引用是外層函數真實作用域的一個子集。
另外從這個實驗還能推測出一點,即外層函數執行結束後是會被回收掉的。因爲既然函數內部變量已被回收,那函數本身也沒有存在的意義了。