iT邦幫忙

DAY 10
8

Javascript系列 第 7

利用closure來達成模組化與私藏資料

沒有開始寫的時候真不曉得closure有這麼多東西好寫...

今天的資料來源是
Javascript: The Good Parts 中文版 page 41-42
Javascript Effective 中的 Item 35

大家如果手邊有書,可以直接看書,我只是記錄一下自己的理解過程。
我們都知道 javascript 的物件跟別人的不太一樣,有很多不一樣,今天要聊的這個不一樣是它沒有很直覺的私有變數的概念,如果想要控制一些私有變數的操作,在 javascript 中,並不是那麼容易。

function User(name, passwordHash){
  this.name = name;
  this.passwordHash = passwordHash;
  this.toString = function(){
    return "[User " + this.name + "]";
  };
  this.checkPassword = function(password){
    return hash(password) === this.password;
  };
}

var azole = new User('azole', 'abcd');
console.log(azole.toString());  // [User, azole]
console.log(azole.passwordHash);// abcd

例如這樣的一個物件,至少 passwordHash 是不應該要被人知道的,但 javascript 又沒有私有變數的概念,所以除了我們允許的 toString 可存取資料外,也可以直接用物件去存取不想被公開的 passwordHash。

這時候就可以利用 closure,closure 會將資料封閉起來,只有透過closure才可以存取它,進而達到資訊隱藏的目的。這樣說實在很繞口,實作上其實很簡單,我們直接看實例吧,試著把以上的物件的變數改為私有變數。

function User(name, passwordHash){
  //this.name = name;
  //this.passwordHash = passwordHash;
  this.toString = function(){
    return "[User " + name + "]";
  };
  this.checkPassword = function(password){
    return hash(password) === passwordHash;
  };
}

var azole = new User('azole', 'abcd');
console.log(azole.toString());  // [User, azole]
console.log(azole.passwordHash);	// undefined

這邊可以看到我們仍可 toString 去取得 name 的值,這是我們允許的,但如果想要直接存取 passwordHash,卻已經拿不到了,而做法超簡單的,只要把this拿掉就好了。

這樣做為什麼可以達到私有變數的概念呢?這是因為我們不再把資料儲存在物件中了,不再把那些資料當成物件的特性了,我們是把資料存在建構式中,也就是 function User 中,當我們建立一個新的物件 azole 時,其實是在執行 User 這個函式,這時候 name 與 passwordHash 是 User 這個函式的區域變數,當 User 這個函式執行完畢時,這些區域變數本來應該被回收的,但因為有被內層函式 toString 與 checkPassword 取用到,所以這 name、passwordHash 這兩個變數並不會隨著 User 函式的執行完畢而被回收,這兩個變數會被儲存在 closure,只能透過 toString 與 checkPassword 去存取,如此一來,其行為就跟私有變數一樣了。

大家也可以看到,這個模式有一個缺點,就是函式必須被放在實體物件中,這樣會導致重復的方法程式碼,這就看大家使用上的考量,如果是真的必需要被隱藏的資料,犧牲一點也是值得的。

另外,優良部分那本書還有提到一個好處。有時候我們會想要在一個函式中,放一些想要被使用的資料,優良部分那本書舉的例子是:想要擴充String這個物件,在裡頭添加一個函式,這個函式的功用是搜尋字串裡頭的一些特殊字元,並且將其替代,例如想將 quot 取代成 "、將 lt 取代成 <、將 gt 取代成 >,這時候我們需要一個對照表,像是這樣:

var entity = {
  quot: '"',
  lt: '<',
  gt: '>'
};

然後我們想要擴充的函式長這樣:

String.prototype.deentityify = function(){
  // 看不懂就先不用管,反正就是利用 regex 做一個取代的動作
  return this.replace(/&([^&;]+);/g, 
    function(a,b){
      var r = entity[b];  // 這邊用到了 entity 這個對照表
      return typeof r === 'string' ? r : a;
    });
};
// 函式的功用如這個範例,將這串字串轉換成 <">
console.log('<">'.deentityify());	// <">

這個對照表在每次呼叫函式時,都要被執行到,那應該把這個對照表存在哪裡呢?放在 global 讓大家都可以用?我想沒有一本書會這樣建議你...

那放在函式中,這樣每次呼叫都能用,像這樣:

String.prototype.deentityify = function(){
  // 把對照表放進函式裡
  var entity = {
    quot: '"',
    lt: '<',
    gt: '>'
  };
  return this.replace(/&([^&;]+);/g, 
    function(a,b){
      var r = entity[b];
      return typeof r === 'string' ? r : a;
    });
};
console.log('<">'.deentityify());	// <">

這樣是沒有錯,函式能用到,也不是放在全域中,但是,這樣每次呼叫函式時,都要去都要估算一次這個對照表,這個對照表明明是不會變動的,每次使用都要重新估算一次,實在是很浪費,那該怎麼辦呢?這時候,還記得上一篇 memoization 嗎?closure實在是個記憶資料的好物阿,詳情請見以下範例:

String.prototype.deentityify = function(){ 
                               // ^^^^^^^^這稱function 1號好了
  // 把資料表放這裡
  var entity = {
    quot: '"',
    lt: '<',
    gt: '>'
  };

  // 留意這個 return
  return function(){
     // ^^^^^^^^這稱function 2號好了
    return this.replace(/&([^&;]+);/g, 
      function(a,b){
        var r = entity[b];
        return typeof r === 'string' ? r : a;
      });
  };
}();
//^^^ 留意這個括號 
console.log('<">'.deentityify());	// <">

這個函式跟上一個版本長得超像,註解中特別請大家留意那個 return 跟最後的括號,跟上一個版本不同的是,擴充的函式 deentityify 並不是函式一號了,而是函式一號「執行的結果」,最後出現的 () 就是在執行這個函式一號。可以看到的是,對照表示被放在函式一號中,在這個函式一號執行時,被估算了一次,理論上,這樣的區域變數應該會隨著函式一號的結束而被回收。

那函式一號執行的結果是什麼呢?每個函式執行的結果就是該函式的回傳值,而函式一號的回傳值就是函式二號,我們把函式二號指派給了 deentityify 這個變數,爾後每次呼叫執行 deentityify 時,都是在執行函式二號。

而作為函式一號的區域變數 entity (對照表)因為有被函式二號給用上,所以 entity 這個函式一號的區域變數並不會隨著函式一號的執行結束而被回收,反而,每次在函式二號被執行時,都能以 closure 的形式被參考到。

如此一來,是不是很巧妙地達成讓 entity 只被估算一次,卻能讓函式每次呼叫都能被使用,卻又不用放在全域變數中。


上一篇
Javascript Closure的用法 - Memoization
系列文
Javascript7

2 則留言

0
0
葉宇霖
iT邦新手 4 級 ‧ 2013-10-03 01:58:01

closure 太高深莫測了
謝謝小賴的分享
開心開心

azole iT邦新手 5 級 ‧ 2013-10-03 01:59:17 檢舉

不用客氣,感謝你們花時間看,有寫得不好或不對的地方,還請多多指教。

我要留言

立即登入留言