搞清楚lexical scope與closure

 Thu, 04 Sep 2008 13:38:19 +0800

之前沒有徹底搞懂這兩者的差別,今天回頭去看Martin Fowler講Closure的文章以及wikipedia對closure的解釋時,總算弄清楚了(之前偷懶,沒把東西好好看完)。

lexical scope的特性,讓我們可以用區域變數的方式,把變數當作一個function物件的private member,但是又可以用一個function當作getter/setter來存取他。例如:

function Bean() {
var X;
var Y;
this.setX = function(x) {
X=x;
}
this.getX = function() {
return X;
}
this.setY = function(y) {
Y = y;
}
this.getY = function() {
return Y;
}
}
var a = new Bean();
a.setX(3);
a.setY(3);
alert(a.getX()+""+a.getY());

另一個更常用的時機,是使用setTimeout。例如:

function TT() {
this.lm = "the message";
var localthis = this;
this.startT = function() {
setTimeout(function(){alert(localthis.lm);},500);
}
}
var d = new TT("the message");
d.startT();

在傳給setTimeout的匿名函數裡面使用this的話,這個this會指向Window物件,所以要在這裡要先把他指定給一個區域變數,這樣就可以讓存在this.lm裡的訊息,透過localthis來取用。

像這樣可以用函數把區域變數攜出到別的scope使用,還只是lexical scope的應用,要產生closure,需要一些特別的條件:

var m1 = "free variable to be bound.";
var m2 = "another free variable to be bound.";
function a(free) {
return function(){setTimeout(function(){alert(free);},500);}
}
var b = a(m1);
m1 = null;
b();
var c = a(m2);
m2 = null;
c();
b();
c();
(本例有誤,請見下文補充,感謝Josh網友指教。)

執行這一段程式,結果會依序跳出"free variable to be bound", "another free variable to be bound", "free variable to be bound", "another free variable to be bound"訊息。closure發生的要件就是,一個free variable透過一段敘述,結果就被bound在這一段敘述裡了。透過這一段程式可以發現,不論傳給a()什麼,什麼就被綁在裡面了,即是傳給他的變數之後用assign的方式釋放或改變了(理論上),被綁在a()裡面的東西也不會改變。

許多討論javascript closure的網站,都會舉類似下面的意外使用到closure的狀況:

function addEvent(node) {
node.onclick = function() {
node.innerHTML += "bla ";//here the leak started
}
}

像上述的程式,你對多少dom node做處理,就有多少dom node會被keep在記憶體中。數量多的話.....

另外,第一個closure的例子透過b=null;c=null;這樣應該可以釋放被「封絕」的東西;而第二個例子透過node.onclick=null;也應該可以。但是在舊版的IE(6?)就不是這一回事。因為HTML部份跟Script是分開的兩個ActiveX元件,可以做出Closure,但是沒有辦法這樣釋放記憶體......結果就......建議你不要使用Closure。(不然就會變成火炬喔)


2008-10-4 0:24 補充

最近在看Crockford的《Javascript: 優良部分》,看起來他並沒有把「參數傳入函式」當作形成closure的必要條件,所以像「參數傳入函式」這種狀況應該算是closure的一種,只是side effect比較大。


2008-12-15 16:04補充

今天蒙Josh兄指正我的例子有問題(第三個例子),的確也有問題,我不應該用String做例子,修改後如下:

var str = {a:"test"};
function a(v) {
v['a'] = "test2";
}
a(str);
alert(str.a);
function b(v) {
var b = v;
this.getB = function() {
return b.a;
}
}
var c=new b(str);
str = null;
alert(c.getB());

上面這一段code才能表現出我要說的意思,對於DOM物件也是類似的狀況。我另外測試了所有的型別,發現一些有趣的東西,我用另一篇文章寫好了。


2008-12-16 10:28補充

另外寫了一篇「對於Javascript所有型別在Closure中表現的測試」,可以參考一下。