最近更新: 2007-05-24

關於 Closure 和 Anonymous function 的差別

jaceju 在 Anonymous functions in PHP 說某個 PHP 研討會討論了匿名函數 (anonymous function) 在 PHP 中的需求性。 jaceju 注明 Jim Wilson 說匿名函式和 closure 是完全不一樣的東西,而他自己看不出兩者的差別。

我在寫 JavaScript 時,常常碰到這個問題。用 JavaScript 也比較容易說明兩者的差異。

JavaScript 語法支持匿名函數(Anonymous function)[參考The practice of anonymous recursion function in JavaScript以了解匿名函數在 JavaScript 中的應用],但不支持封絕(Closure)[我稱為封絕,也有人稱它閉鎖空間或閉包,還是記住 Closure 這個詞吧]。 JavaScript 必須使用一些技巧實現 Closure ,因此我們反而容易看出這兩者的差別。

var i = 0;
var fs = [];
function run_fs() {
    for (var j = 0, f = fs[j]; f; f = fs[++j]) {
        f();
    }
}

print('Pure anonymous function\n');
for (i = 0; i < 3; ++i) {
    fs[i] = function(){print(i);}
}
run_fs();

print('Closure\n');
for (i = 0; i < 3; ++i) {
    fs[i] = (function(i) {
        //closure.

        return function(){print(i);}
    })(i);
}
run_fs();

建議使用 JavaScript shell 執行上面這段程式碼。若有人想用 .Net 的 JScript compiler (jsc.exe) 編譯再執行,也可以啦。其結果如下所示:

Pure anonymous function
3
3
3
Closure
0
1
2

第一段 (line 3~7) 實際上是在陣列 fs 中儲存了 3 個純匿名函數的參照,這3個函數參照應該指向同一段程式碼 (content)。當它們執行 print(i); 時,使用的是第1行定義的 i 。此時, i 之值為 3 ,故3個函數的執行結果都是 3 。

第二段 (line 17~20) 的寫法就不一樣了,此一寫法產生了一個 Closure 。 Closure 擁有一個獨立的 content 儲存其中的局部內容。故在此例中的 i 實際上是儲存在一個與外界隔離的 content 之中的區域變數,並不是第一行所定義的 i。同樣在此 Closure 之中的匿名函數,將使用這個被隔離的區域變數 i。由於此時這3個獨立的content中的 i 之值不同,故每次函數的執行結果都不同。

順便提一下記憶體回收的事。在第二段定義並執行了3次匿名函數實例。這3個匿名函數實例各自產生了一個獨立的 content 。一般情形時,執行之後其 content 內容就會被系統收回。但在此例中,我又在這 content 中定義了一個匿名函數 (第19行),並將之回傳後儲存在 fs 陣列。由於 content 中的資源 - 即其中的匿名函數 - 被尚存在的資源 fs 所指涉,故不會被系統回收。直到 fs 消逝之後,系統才會收回此例所產生的3個獨立 content 佔有的記憶體。


補充一份參考文件。由 Martin Fowler 寫的 《Closure》。Fowler 也寫了一個 JavaScript 的例子。因為他寫的例子有點長,我改了一個簡潔版,如下所示:

function Employee() {
}

function paidMore(amount) {
	return function(e){ return e.salary > amount; };
}

var highPaid = paidMore(150);

var john = new Employee();
john.salary = 200;
print(highPaid(john));

我的例子是用匿名函數創造 Closure ,而 Fowler 的例子則是用具名函數 paidMore() 創造 Closure 。

樂多舊網址: http://blog.roodo.com/rocksaying/archives/3337623.html

樂多舊回應
未留名 (#comment-10615677)
Thu, 24 May 2007 15:28:47 +0800
看到這行:

(function(i) {
//closure.
return function(){print(i);}
})

大概懂意思了,感謝石頭成老大的解說 :)
未留名 (#comment-10615689)
Thu, 24 May 2007 15:31:32 +0800
之前是看這篇:

http://www.javaworld.com.tw/roller/page/ingramchen?entry=2007_1_1_WhyAddClosureInJava7

不過會有點迷迷糊糊...用 JavaScript 反而看得懂...真是怪了...
未留名 (#comment-10616765)
Thu, 24 May 2007 17:54:39 +0800
Java陣營基於一種對語法的古怪堅持 (雖然我現在就覺得它的語法又臭又長) ,現在連 anonymous function 都不支持。要解說本例之前,還要先用 anonymous inner class 模擬 anonymous function 。接著再用模擬的 anonymous function 再去模擬 closure 。如此蹩腳的寫法如果還能像 JavaScript 一樣簡潔地表示本文要說明的內容,那也太不可思議了。

另外,我搜尋到 Martin Fowler 寫的 Closure 。他也寫了一個 JavaScript 的例子,我補到正文去了。
未留名 (#comment-10617075)
Thu, 24 May 2007 18:50:46 +0800
我在看Paul Graham的Lisp介紹中提到Closure,他用Javascript寫了一個累加程式範例

function(n){
return function(i){return n+=i;};}

讓我頓悟了0rz
未留名 (#comment-10618303)
Thu, 24 May 2007 21:12:23 +0800
R@Ndy ,你提供的程式碼很有趣,但是不是省略了什麼?
例如說,是不是像下面這樣用?

var f = function(n){
return function(i){return n+=i;};
}

print( (f(2))(3) );
//In this case, n = 2, i = 3

未留名 (#comment-14510601)
Wed, 19 Sep 2007 18:08:39 +0800
修正一下,方便說明:
var i = 0;
var fs = [];
function run_fs() {
for (var j = 0, f = fs[j]; f; f = fs[++j]) {
f();
}
}

for (i = 0; i < 3; ++i) {
fs[i] = function a(){print(i);}
}
run_fs();

for (i = 0; i < 3; ++i) {
fs[i] = (function b() {
var j = i * 3;
return function c(){print(j);}
})();
}
run_fs();

function e() {
var j = i * 2;
return function f(){print(j);}
}
for (i = 0; i < 3; ++i) {
fs[i] = e();
}
run_fs();

以 ECMAScript 3 Specification 的說法,
每一個客製 function 都有一個叫 [[Scope]] 的內部屬性紀錄了那個 function 在定義或宣告時的 scope chain ,每當呼叫 function 進入 function code 時,會產生一個叫 activation object 的新物件作為 function scope ,並把這個物件放在由 [[Scope]] 得到的 scope chain 的前頭,形成一個新的 scope chain ,function code 就是用這個新 scope chain 進行識別子解析(identifier resolution)。

根據數學定義, scope chain 就是 closure,而每次離開 function code ,可能被GC回收的是 activation object 這個 function scope,亦即是因為每次進入 function code 時,function scope 都是不同的,因而有不同 closure 。
函數 a 參照的 i 是在 a 的 [[Scope]] 中global object的變數 i ,因為雖然每次定義和呼叫的 a 是不同的 a ,但是是同一個 global object ,所以是參照到同一個 i 。
而函數 f 參照的 j 是 f 的 [[Scope]] 中屬於函數 e 的 activation object 的變數 j , 因為雖然每次呼叫的 e 是同一個 e ,但是是不同的 activation object ,所以是不同的 j 。

根據數學定義,其實很多東西都是 closure ,例如 propotype chain。
所謂封閉是相對的,而這裡是相對於識別子解析或屬性提取(property get,根據EMCAScript v3 的規格書是物件的內部方法 [[Get]] )。
scope chain 中的物件不一定是 activation object 或 global object,也可以透過 with statement 加入其他物件。

如果這樣理解,class-based 的繼承關係形成的繼承鏈也是 closure (如果不理私有屬性的話),就如同 propotype chain。
未留名 (#comment-14511005)
Wed, 19 Sep 2007 19:34:56 +0800
串錯字,是 prototype ,不是 propotype 。