閉包(Closure)



閉包(Closure)
是擁有閒置變數(Free variable)的運算式。閒置變數真正扮演的角色依當時語彙環境(Lexical environment)而定。支援閉包的程式語言通常具有一級函式(First-class function)。建立函式不等於建立閉包。如果函式的閒置變數與當時語彙環境綁定,該函式才稱為閉包。

那麼何為閒置變數?閒置變數是指對於函式而言,既非區域變數也非參數的變數,像區域變數或參數,其作用範圍基本上在被定義的函式範 圍中。它是被綁定變數(Bound variable)。舉個例子來說:
js> function doSome() {
  >     var x = 10;
  >     function f(y) {
  >         return x + y;
  >     }
  >     return f;
  > }
js> var foo = doSome();
js> foo(20);
30
js> foo(30);
40
js>


上面doSome的例子中,f建立了一個閉包,如果你單看:
function f(y) {
    return x + y;
}

看來起x似乎沒有定義。實際上,x是從外部函式捕捉而來。閉包是個捕捉了外部函式變數(或使之繼續存活)的函式。在上例中,函式f建立了閉包,因為它將變數x關入(close)自己的範圍。如果形式閉包的函式物件持續存活,被關閉的變數x也會繼續存活。就像是延續了變數x的生命週期。

由 於doSome傳回了函式物件並指定給foo,就doSome而言已經執行完畢。單 看x的話,理應x已結束其生命週期,但由於doSome中建立了閉包並傳回,x被關閉在閉包中,所以x的生命週期就與閉包的生命週期相同了。如上例所示, 呼叫foo(20)結果就是10+20(因 為被閉關的x值是10),呼叫foo(30)結果就是10+30。

注意!閉包關閉的是變數,而不是變數所參考的值。下面這個範例可以證明:
js> function doOther() {
  >     var x = 10;
  >     function f(y) {
  >         return x + y;
  >     }
  >     x = 100;
  >     return f;
  > }
js> var foo = doOther();
js> foo(20);
120
js> foo(30);
130
js>


建立閉包時,綁定了x變數,而不是數值10(x變數的值),也因此doOther之後改變了x變數的值,而後傳回閉包給foo參數後,範例顯示的結果分別是100+20與100+30。由於閉包綁定的是變數,所以你也可以在閉包中改變變數的值:
js> var sum = 0;
js> [1, 2, 3, 4, 5].forEach(function(element) {
  >     sum += element;
  > });
js> sum;
15
js>


你可能會有疑問的是,如果閉包關閉了某個變數,使得該變數的生命週期得以延長,那麼這個會怎麼樣?
js> function doOther() {
  >     var x = 10;
  >     function f() {
  >         x--;
  >         return x;
  >     }
  >     return f;
  > }
js> var f1 = doOther();
js> var f2 = doOther();
js> f1();
9
js> f2();
9
js>


在這個範例中,doOther被呼叫了兩次(或更多次),doOther中的閉包關閉了x,並對其執行了遞減。呼叫了f1時,x會被遞減1,所以顯示9,這沒有問題,那麼呼叫f2()後,結果是9?

像這類的例子,其實結果是很一致的,關閉的是建立閉包時外部範圍下的變數。以上例來說,第一次呼叫doOther時,建立了x變數,指定值給x變數,而後建立閉包將之關閉。第二次呼叫doOther時,建立了x變數,指定值給x變數,而後建立閉包將之關閉。所以f1與f2關閉的根本是不同作用範圍的x變數(也就是該次呼叫doOther時所建立的x變數)。所以上例中,呼叫f2之後顯示的值仍是9。

下面這個也是個類似的例子:
js> function doSome(x) {
  >     return function(a) {
  >                 return x + a;
  >            };
  > }
js> var f1 = doSome(100);
js> var f2 = doSome(200);
js> f1(10);
110
js> f2(10);
210
js>


閉包的實際應用很多,例如,在 因式分解 中,可讓閉包綁定質數表,之後就不用重複建立質數表:
  • factor.js
function prepareFactor(max) {
var prime = new Array(max + 1);
for(var i = 2; i * i <= max; i++) {
if(prime[i] == undefined) {
for(var j = 2 * i; j <= max; j++) {
if(j % i == 0) {
prime[j] = 1;
}
}
}
}
var primes = [];
for(var i = 2; i <= max; i++) {
if(prime[i] == undefined) {
primes.push(i);
}
}
// factor 會綁定 primes
function factor(num) {
var list = [];
for(var i = 0; primes[i] * primes[i] <= num;) {
if(num % primes[i] == 0) {
list.push(primes[i]);
num /= primes[i];
}
else {
i++
}
}
list.push(num);
return list;
}
return factor;
}

primes被閉包關閉,所以自然而取得primes參考的物件。你可以這麼使用:
js> load('factor.js');
js> factor = prepareFactor(1000);
function factor(num) {
    var list = [];
    for (var i = 0; primes[i] * primes[i] <= num; ) {
        if (num % primes[i] == 0) {
            list.push(primes[i]);
            num /= primes[i];
        } else {
            i++;
        }
    }
    list.push(num);
    return list;
}

js> factor(100);
2,2,5,5
js> factor(200);
2,2,2,5,5
js>


閉包也會用來作為物件私用(private)的模擬,以及名稱空間的管理等,這之後還會再看到說明。