Closure 與一級函式
July 24, 2022Closure 是擁有閒置變數(Free variable)的運算式。閒置變數真正扮演的角色依當時環境而定。支援 Closure 的程式語言通常具有一級函式(First-class function)。建立函式不等於建立 Closure 。如果函式的閒置變數與當時環境綁定,該函式才稱為 Closure。
閒置變數
那麼何為閒置變數?閒置變數是指對於被建立的函式而言,既非區域變數也非參數的變數。舉個例子來說:
function doSome() {
var x = 10;
function f(y) {
return x + y;
}
return f;
}
var foo = doSome();
console.log(foo(20)); // 30
console.log(foo(30)); // 40
上面的函式 doSome
中,函式 f
建立了一個 Closure,如果你單看:
function f(y) {
return x + y;
}
看來起變數 x
似乎沒有定義。實際上,x
是從外部函式捕捉而來。 Closure 是個捕捉了外部函式變數(或使之繼續存活)的函式。在上例中,函式 f
建立了 Closure,因為它將變數 x
關入(close)自己的範圍,這也是 Closure 這個名稱的由來。如果形成 Closure 的函式物件持續存活,被關閉的變數 x
也會繼續存活。就像是延續了變數 x
的生命週期。
由於 doSome
傳回了函式物件並指定給 foo
,就 doSome
而言已經執行完畢。單看 x
的話,理應 x
已結束其生命週期,但由於doSome
中建立了 Closure 並傳回,x
被關閉在 Closure 中,所以 x
的生命週期就與 Closure 的生命週期相同了。如上例所示,呼叫 foo(20)
結果就是 10 + 20
(因為被閉關的 x
值是 10),呼叫 foo(30)
結果就是 10 + 30
。
如果沒有捕捉任何變數,那麼就是單純的(一級)函式而已。例如,在下面的例子中,函式 f
沒有形成 Closure,因為它沒有捕捉外部任何變數:
function doSome() {
var x = 10;
function f(y) {
return y + 10;
}
return f;
}
捕捉變數
注意! Closure 關閉的是變數,而不是變數所參考的值。下面這個範例可以證明:
function doSome() {
var x = 10;
function f(y) {
x = x + y;
return x;
}
return f;
}
var foo = doSome();
console.log(foo(20)); // 30
console.log(foo(30)); // 60
建立函式 f
時綁定了 x
變數,形成了一個 Closure,綁定的是 x
變數,而不是數值 10(x
變數的值),因此,第一次呼叫 foo(20)
後,x
的值成了 10 + 20
,再次呼叫 foo(30)
時,x
的值原本是 30,在 x = x + y
之後,x
的值就成了 30 + 30
。
由於 Closure 綁定的是變數,你才可以在 Closure 中改變了被綁定變數的值,以下是另一個例子:
var sum = 0;
[1, 2, 3, 4, 5].forEach(function(elem) {
sum += elem;
});
console.log(sum); // 15
你在 Closure 中改變了 sum
的值,forEach
執行完之後,取得 sum
的值也才會是 15。
你可能會有疑問的是,如果 Closure 關閉了某個變數,使得該變數的生命週期得以延長,那麼這個會怎麼樣?
function doSome() {
var x = 10;
function f(y) {
x = x + y;
return x;
}
return f;
}
var foo1 = doSome();
var foo2 = doSome();
console.log(foo1(20)); // 30
console.log(foo2(20)); // 30
在這個範例中,doSome
被呼叫了兩次,每次呼叫時其實都建立了個別的區域變數 x
,而個別建立的 Closure 關閉了個別的 x
。foo1
與 foo2
中的 x
彼此並不影響。
Closure 的應用很多,在 JavaScript 中常見用於物件私用(private)的模擬,以及名稱空間的管理等,這之後還會再看到說明。