本文不談閉包的概念,由於概念容易把人搞暈,本文但願經過幾個鮮活的例子來探究閉包的性質,相信對理解閉包會有所幫助。數組
程序1閉包
var f = (function() { var n = 10; return function() { ++n; console.log(n); } })(); f();
輸出:dom
11
結論:函數
閉包函數能夠訪問外層函數中的變量。spa
程序23d
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(); }
輸出:code
1 2 3
結論:對象
一個函數內有多個閉包函數,那麼這些閉包函數共享外層函數中的變量。能夠看出,例子中3個閉包函數中的n是同一個變量,而不是該變量的3個副本。 blog
程序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」的結論:閉包函數對外層函數做用域的引用是外層函數真實做用域的一個子集。
另外從這個實驗還能推測出一點,即外層函數執行結束後是會被回收掉的。由於既然函數內部變量已被回收,那函數自己也沒有存在的意義了。