什么是閉包
簡單的說閉包就是函數里面的函數,《JavaScript高級程序設計》里是這樣定義的
閉包是指有權訪問另一個函數作用域中的變量的函數。
先看一道面試時經常被考的題目
- 代碼1:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>lzhTest</title>
</head>
<body>
<ul>
<li>0</li>
<li>1</li>
</ul>
<script>
var lis = document.getElementsByTagName("li");
for(var i = 0; i < lis.length; i++){
lis[i].onclick = function(event){
alert(i);
}
}
</script>
</body>
</html>
分別點擊 li,alert什么?答案均是 2. 為什么呢?我們接着往下看
作用域鏈和活動對象
函數被調用時會創建一個執行環境和作用域鏈 (scope chain),作用域鏈中每個元素都指向一個活動對象或變量對象 (執行環境中定義的所有變量和函數都保存在這個對象中,包括 this、arguments),函數執行完畢,作用域鏈被銷毀,如果這時相應的變量對象沒有被引用,則變量對象占用的空間會被釋放。
比如上面題目中的作用域鏈是這樣的:
匿名函數1
和 匿名函數2
是兩個事件處理函數,從圖中可以看出,在作用域鏈的最前端(即下標為0)對應的活動對象中,是不存在 i
的,i
在全局變量對象中,點擊的時候,需要往作用域的上層查找 i
,於是就找到了全局變量對象中的 i
,因為點擊的時候,i
早已增加成為 2,所以 alert 的 i
均為 2。
怎樣做到每次 alert 的是下標呢?
直接看代碼:
- 代碼2
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>lzhTest</title>
</head>
<body>
<ul>
<li>0</li>
<li>1</li>
</ul>
<script>
var lis = document.getElementsByTagName("li");
var helper = function(i){
return function(event) {
alert(i);
}
}
for(var i = 0; i < lis.length; i++){
lis[i].onclick = helper(i);
}
// 參考自:《JavaScript語言精粹》
</script>
</body>
</html>
對應的作用域鏈及活動對象:
- 代碼2中,全局變量對象中 i 的變化用 0->2 表示 (代碼1也如此)
- 第一次調用 helper 時,全局變量對象中 i 是0,所以此時 helper(1) 的活動對象中 i 是0,因為是以形參的形式從全局變量對象中傳進來的。此后 helper(1) 中的 i 就不變了。
- 接着 helper(1) 中返回一個
匿名函數1
,(根據《JavaScript高級程序設計》介紹:函數在調用時生成作用域鏈和活動對象,但閉包被返回時,就會生成作用域鏈和活動對象(中文第3版 P180 line7),我的理解並不是這樣的),返回的匿名函數1
引用着 helper1 中的i,helper(1) 執行完畢,之后 helper(1)的執行環境和作用域鏈銷毀,但是 helper(1) 的活動對象還在,因為匿名函數1
的作用域鏈還在引用着它(按照我的理解應該是匿名函數1
還引用着它)。(執行環境和作用域鏈銷毀這一過程在圖中沒有體現出來)。 - 如果用戶 click lis[0],那么就會調用
匿名函數1
,(按照我的理解:此時匿名函數1的執行環境才會被壓入環境棧中,同時生成匿名函數1
的作用域鏈和活動對象),alert(i)
時,因為匿名函數1
的活動對象中找不到i
所以往作用域鏈的父級找,找到了 helper(1) 活動對象中的 i,於是alert
了 0 - 在第二次調用 helper 時,生成的作用域鏈和活動對象是新的了,與 helper(1) 中的不同,同理,當用戶 click lis[1] 時,
alert 1
,如果此時還不是很懂的話,可以回頭再看看圖,或者從 代碼1 那里從新理解。 - 如果有認真看的話,請思考一下我的看法和《JavaScript高級程序設計》的看法,到底哪個是正確的,我還是堅持自己的看法,如果有錯的話,還請指出。
由於閉包會攜帶包含它的函數的作用域,因此會比其他函數占用更多的內存。過度使用閉包可能會導致內存占用過多。
- 來看一個閉包內引用着活動對象中的變量時,活動對象不被釋放的例子,注意看圖片右側的 scope:
從圖中可以看出,param1、param3 所在的活動對象不在作用域鏈中,應該是被通知准備回收了或者已經回收了,而 param2、param4 所在的變量對象還在。
-
另外,如果閉包中有
eval
的話,由於不能判斷eval
里是否有引用父級作用域鏈活動對象中的變量,那么該作用域鏈中的所有活動對象都會被保留,所以在閉包中盡量不要使用eval
:
-
但如果在里面用的是
new Function("這里引用閉包外的變量")
這種寫法,如果沒有其它引用,父級作用域鏈的活動對象是不會保留的,下面這種寫法最終會報錯Uncaught ReferenceError: param1 is not defined
:
內存泄露
如果閉包的作用域鏈中保存着一個HTML 元素,那么就意味着該元素將無法被銷毀,代碼如下,只要匿名函數存在,element 的引用數至少也是 1,因此它所占用的內存就永遠不會被回收。(書中有討論到這是IE9以前的問題,我怎么覺得這是個普遍的問題呢,難道 Chrome 或其它瀏覽器中 element 的引用數至少還能是0嗎?求不吝賜教)。
function assignHandler(){
var element = document.getElementById("someElement");
element.onclick = function(){
alert(element.id);
};
}
- 所以《JavaScript高級程序設計》建議這么寫:
function assignHandler(){
var element = document.getElementById("someElement");
var id = element.id;
element.onclick = function(){
alert(id);
};
element = null;
}
閉包的作用
閉包無處不在,如果真的要歸納幾個用處,如下:
模仿塊級作用域
我們都知道,在 JavaScript 中,是不存在塊級作用域的,也就是在 {} 里面聲明的變量,在 {} 外面依然可以訪問。但如果利用函數一旦執行完,其中執行環境和作用域鏈均銷毀的特性,我們可以這么做:
(function(){
// 讓一個括號包着一個函數,相當於得到這個函數的引用,
// 然后再在后面加個括號,執行這個函數,稱這種函數為 立即執行函數
// 在這里面聲明的變量,外部不可訪問,除非 return 一個閉包或變量
// 如果此時外部是一個函數的話,那這個立即執行函數也是一個閉包
// 只是這個閉包並沒有返回些什么
})();
模塊化開發
有了上面提到的模仿塊級作用域,就可以減少全局變量的使用,jQuery 就是這么做的:
(function(window, undefined){
// ...
// 這里實現 jQuery 的所有功能
// 勢必會聲明很多變量,如果暴露在全局作用域中會造成命名沖突
// 所以用一個立即執行函數包起來,但是為什么要傳入 window 呢
// 傳入 window 是為了讓 window 成為當前作用域下的變量,這樣可以減少訪問成本,
// 另外便於壓縮,比如將 window 壓縮成 e,外部傳進來的依然是 window
// 傳入 undefined 的原因:
// 在低版本的 IE 中,undefined 是可寫的,有可能 undefined 就不是 undefined 了
window.jQuery = window.$ = jQuery;
// 這里的 jQuery 就是一個函數,平時我們用的時候是 $(參數、參數),所以,他也是個閉包嘛
// 上面是模塊中的一種,而旦還有更高級的,jQuery 兼容 AMD 規范
if ( typeof define === "function" && define.amd && define.amd.jQuery ) {
define( "jquery", [], function () {
// AMD 這里不詳細介紹了,這里就是 define 一個模塊
// 這里也是一個閉包
return jQuery; // 這里就是閉包中的閉包了
} );
}
}(window))
- 閉包無處不在