iT邦幫忙

DAY 2
4

Javascript面面觀系列 第 2

Javascript面面觀:核心篇《閉包》

  • 分享至 

  • xImage
  •  

閉包(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>

這裡面需要注意幾點現象:

  1. a函數雖然執行完畢,但是因為裡面定義的函數被指派為傳入的elem參數(是一個dom node)事件處理函數,這個事件處理函數沒有結束,所以a函數裡面的變數elem及v,都會一直保持在執行時的狀態。
  2. 事件函數執行的context是在定義他的函數外,但他仍然可以取得在定義他的函數內的變數。同時他還可以透過this取得他所在的context物件的property。
  3. a()函數執行兩次,每次執行都會保存執行時的變數狀態。

歷史問題

在網路上查詢閉包,可能會發現討論記憶體洩漏的問題,於是有些人不建議使用閉包。

這個問題之所以會發生,主要是因為早期的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以釋放記憶體。所以使用閉包時還是要小心,不要無意間使用,應該權衡利弊後明確地使用。


上一篇
Javascript面面觀:核心篇《變數範圍》
下一篇
Javascript面面觀:核心篇《高階函數》
系列文
Javascript面面觀30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
fillano
iT邦超人 1 級 ‧ 2009-10-14 17:50:39

有沒有人把closure翻譯成「封絕」的阿?

jamesjan iT邦高手 1 級 ‧ 2009-10-14 17:56:43 檢舉

「閉包」給他有點難聽滴咧...因為想反的詞意..叫...XD

fillano iT邦超人 1 級 ‧ 2009-10-14 19:58:47 檢舉

哇哈哈...

我要留言

立即登入留言