2018年1月2日 星期二

[ JavaScript Gossip ] JavaScript 核心 : 一切都與函式有關 (閉包 Closure)

轉載自 這理 
前言 : 
所謂的閉包(Closure,是指一個函式物件(或函式值)在建立時,綁定了當時作用範圍(Scope)下有效的自由變數(Free variable。所 以支援閉包的語言,必須有支援一級函式(First-class function),建立函式物件並不等於建立閉包,建立函式物件時同時綁定了某個()自由變數,該函式物件才稱之為閉包. 

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

上面 doSome 的例子中,f 形成了一個閉包,如果你單看 : 
  1. function f(y) {  
  2.     return x + y;  
  3. }  
x 沒有任何的意義,其語意依賴於當時 x 作用範圍下的意義,閉包必須在自由變數的有效範圍時定義. 在上面 doSome 的例子中,f 形成了一個閉包,因為這個函式物件將自由變數 x 關閉(closed over)在函式物件自己的作用範圍中,只有函式物件還有效,被關閉的自由變數就有效,相當於延長了自由變數的作用範圍

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

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

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

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

在範例中,doOther 被呼叫了兩次(或更多次),doOther 中的閉包關閉了 x,並對其執行了遞減。呼叫了 f1 時,x 會被遞減1,所以顯示 9,這沒有問題,那麼呼叫 f2 後,結果還是9! 像這類的例子,其實結果是很一致的,關閉的是建立閉包時有效範圍下的自由變數. 以上例來說,第一次呼叫 doOther 時,建立了 x 變數,指定值給 x 變數,而後建立閉包將之關閉。第二次呼叫 doOther 時,建立了 x 變數,指定值給 x 變數,而後建立閉包將之關閉。所以 f1 與 f2 關閉的根本是不同作用範圍的 x 變數!(也就是該次呼叫 doOther 時所建立的 x 變數).所以上例中,呼叫 f2 之後顯示的值仍是 9. 下面這個也是個類似的例子 : 
> function doSome(x){ return function(a){ return x + a; } }
> var f1 = doSome(100);
> var f2 = doSome(200);
> f1(10);
110
> f2(10);
210

閉包應用 : 
閉包的實際應用很多,例如,在 因式分解 中,可讓閉包綁定質數表,之後就不用重複建立質數表 : 
- factor.js :
  1. function prepareFactor(max) {  
  2.      var prime = new Array(max + 1);  
  3.      for(var i = 2; i * i <= max; i++) {  
  4.          if(prime[i] == undefined) {  
  5.              for(var j = 2 * i; j <= max; j++) {  
  6.                  if(j % i == 0) {  
  7.                      prime[j] = 1;  
  8.                  }  
  9.              }  
  10.          }  
  11.      }  
  12.      var primes = [];  
  13.      for(var i = 2; i <= max; i++) {  
  14.          if(prime[i] == undefined) {  
  15.              primes.push(i);  
  16.          }  
  17.      }  
  18.      // factor 會綁定 primes  
  19.      function factor(num) {  
  20.          var list = [];  
  21.          for(var i = 0; primes[i] * primes[i] <= num;) {  
  22.              if(num % primes[i] == 0) {  
  23.                  list.push(primes[i]);  
  24.                  num /= primes[i];  
  25.              }  
  26.              else {  
  27.                  i++  
  28.              }  
  29.          }  
  30.          list.push(num);  
  31.          return list;  
  32.      }  
  33.      return factor;  
  34. }  
  35.   
  36. exports.test = prepareFactor  

primes 被閉包綁定,所以 primes 所參考的物件自然也就被綁定. 你可以這麼使用 : 
> var t = require('./factor.js')
undefined
> t
{ test: [Function: prepareFactor] }
> factor = t.test(1000)
[Function: factor]
> factor(200)
[ 2, 2, 2, 5, 5 ]

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

沒有留言:

張貼留言

[Linux 常見問題] How to set a file size limit for a directory?

Source From   Here   Question   I   have a directory on my system which is used for a specific reason by applications and users , but I   d...