沒有開始寫的時候真不曉得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 只被估算一次,卻能讓函式每次呼叫都能被使用,卻又不用放在全域變數中。
closure 太高深莫測了
謝謝小賴的分享
不用客氣,感謝你們花時間看,有寫得不好或不對的地方,還請多多指教。