閉包(closure)是非常好用的功能,可以用非常簡單的方式解決變數範圍、取值等問題。不過使用上也有一些潛在問題,所以要權衡得失。
閉包的定義
在Javascript裡面,只要有巢狀的函數定義,就會形成閉包。因為內層的函數需要引用到外層函數中定義的變數(建立範圍鏈scope chain),所以外層函數中變數的狀態就好像被內層函數「關閉」起來了,所以有了這個名稱。
(可以參考wiki上的解釋:http://en.wikipedia.org/wiki/Closure_(computer_science),另外對於歷史有興趣的話,可以看看這一篇文章:http://gafter.blogspot.com/2007/01/definition-of-closures.html)
常見的閉包
先看個程式,例如想用一個函數來對dom node產生事件處理函數,可能會這樣做:
<div id="target" style="width:100px;border:solid 1px black">test</div>
<div id="panel" style="width:100px;border:solid 1px black">test2</div>
<script>
var a = function(elem) {
var v = 'test';
elem.onclick = function() {
alert(v + ": " + elem.id + " eq " + this.id);
}
}
a(document.getElementById('target'));
a(document.getElementById('panel'));
</script>
這裡面需要注意幾點現象:
歷史問題
在網路上查詢閉包,可能會發現討論記憶體洩漏的問題,於是有些人不建議使用閉包。
這個問題之所以會發生,主要是因為早期的IE中,Javascript跟DOM是從不同的元件來的東西,結果在閉包的狀況中,上例的elem跟v變數使用的記憶體在瀏覽器關閉後不會被正確釋放。目前的IE應該都已經修正了這個問題,所以不用太擔心。
活用閉包
善用閉包,就可以用非常精簡的程式解決變數範圍及變數值變動的問題。例如下面這個程式:
<div id="target" style="width:100px; border: solid 1px black;cursor: hand">target</div><br>
<div id="test" style="width:100px; border: solid 1px black;cursor: hand">test</div>
<script>
function em() {
this.elem=null;
this.msg='';
this.set=function(node,msg) {
this.elem = node;
this.msg = msg;
};
this.click=function() {
this.elem.onclick=function(){
alert(this.msg);
};
};
}
var a = new em();
a.set(document.getElementById('target'), 'this is message for target');
a.click();//點選target就會出現undefined
function em1() {
this.elem=null;
this.msg='';
this.set=function(node,msg) {
this.elem = node;
this.msg = msg;
};
this.click=function() {
this.elem.onclick=function(o){
return function() {
alert(o.msg);
};
}(this);
};
}
var b = new em1();
b.set(document.getElementById('test'), 'this is message for test');
b.click();//點選test就會出現'this is message for test'
</script>
在node的事件處理函數中,this其實是指向這個node物件而不是原先定義函數時的context中的em函數物件的instance,所以直接用this.msg就取不到東西,因為node上面並沒有這個property。在em1函數物件的instance中,改成把this當作函數參數傳進去匿名函數變成他的本地變數o,再透過o.msg就可以取到想要的訊息。(另一個常見的問題,會發生在使用setTimeout/setInterval。這兩個函數是屬於window物件的方法,傳給它的函數會在global變數範圍裡面執行,this會指向window而不是原來打算使用的context。)
另一個例子,是用closure來捕捉執行時期變數的狀態:
<input type="button" value="test1"/>
<input type="button" value="test2"/>
<input type="button" value="test3"/>
<script>
var a = document.getElementsByTagName("input");
var i=0;
for (i=0; i< a.length; i++) {
a[i].onclick = function(n) {
return function(e) {
alert(n);
};
}(i);
}
</script>
這個例子用到了返回一個函數的技巧,這個主題其實主要是在下一次分享才會提到,但是為了示範閉包的效果,所以還是先拿來用。
外層的匿名函數會透過他的參數n保存傳進來的i變數的狀態,實際傳給按鈕的onclick事件處理函數是最內層的函數,這樣才能正確取得迴圈的值。如果直接取i,取到的是離開for迴圈時變數i的最後計算的結果,這樣三個按鈕按下取的反應就都一樣了。
謹慎使用閉包
在目前的瀏覽器中使用閉包,儘管不致於造成記憶體洩漏,但是還是會佔用記憶體,因為他會讓一些函數在執行完畢後,使用的本地變數仍無法被GC以釋放記憶體。所以使用閉包時還是要小心,不要無意間使用,應該權衡利弊後明確地使用。
有沒有人把closure翻譯成「封絕」的阿?
「閉包」給他有點難聽滴咧...因為想反的詞意..叫...XD
哇哈哈...