JavaScript Design Pattern - Singleton 單體模式
JavaScript Design Pattern 「Singleton 單體模式」 筆記。
Singleton的概念就是同一個class只能建立唯一一個實體物件。當第二次使用同一個class建立新物件的時候,我們會得到和第一次建立時同一個物件。一般來說,當我們利用同一個class建立實體物件時,每次建立得到的物件都會是不同的,例如:
var obj = {
myProp: 'myValue'
};
var obj2 = {
myProp: 'myValue'
};
console.log(obj === obj2); //false
console.log(obj == obj2); //false
而我們預期Singleton可以得到以下的效果...
function Universe(){
//no-op
};
var uni = new Universe();
var uni2 = new Universe();
console.log(uni === uni2); //true
實作方法
- 儲存在靜態屬性中的實體
- 儲存在Closure中的實體
儲存在靜態屬性中的實體
將快取建立在建構式的靜態屬性中(Instance in a Static Property),意即,建立類似 Universe.instance
的屬性,並將物件實體儲存於此。缺點是instance屬性可被public存取,很可能會被修改,而失去應有的正確性。
function Universe(){
if(typeof Universe.instance === 'object'){
return Universe.instance;
}
this.start_time = 0;
this.bang = 'Big';
Universe.instance = this;
};
var uni = new Universe();
var uni2 = new Universe();
console.log('uni === uni2');
console.log(uni === uni2); //true
我們先檢查這個物件是否有被建立過,由於這個物件是存在屬性 Universe.instance
中,因此檢查這個屬性是否有值且存入一個物件。如果有,那就回傳已經建立過的物件;如果沒有,則建立新物件,並存在屬性 Universe.instance
裡面,然後回傳這個新物件。由於函式會預設回傳目前的物件,因此我們不見得要寫return這個指令。在上面這個範例中,我們看到uni和uni2是相等的。
但這樣的實作方式會有個缺點,由於instance屬性是公開的,大家都可以修改,如果有其他程式碼修改了,例如下面的範例,修改值為null,那麼uni3就會收到一個新物件,而讓uni不等於uni3了。
Universe.instance = null; //公開屬性,被修改了...
var uni3 = new Universe();
console.log(uni === uni3); //false
儲存在Closure中的實體
為了解決上面不被公開修改的缺點,我們改將這份實體包在一個closure裡面(Instance in a Closure),保持其為private的狀態,無法在建構式之外被修改,就能保證為唯一。
function UniverseC(){
var instance = this;
this.start_time = 0;
this.bang = 'Big';
//重新定義建構式
UniverseC = function(){
return instance;
};
};
var uni4 = new UniverseC();
var uni5 = new UniverseC();
console.log('uni4 === uni5');
console.log(uni4 === uni5); //true
原本的建構是在第一次才會呼叫做初始化的動作,即產生一份實體物件,而之後呼叫就只是回傳這份實體物件而已。這個觀念即是「Self-Defining-Function」(自我定義函式),可參考程式碼。 但缺點是,重新定義的函式會失去在重新定義後加上去的屬性,即我們失去了inEverything。
//測試是否遺失之後加上去的屬性
UniverseC.prototype.inEverything = true;
var uni6 = new UniverseC();
console.log(uni4.inNothing); //true
console.log(uni4.inEverything); //undefined
console.log(uni6.inNothing); //true
console.log(uni6.inEverything); //undefined
另外,我們也可以觀察到,一個有趣的現象... uni4.constructor
和 UniverseC居然是不一樣的。
console.log(uni4.constructor.name); //UniverseC
console.log(uni4.constructor === UniverseC); //false
console.log(uni4.constructor); //最初定義的UniverseC
console.log(UniverseC); //重新定義的UniverseC
修改儲存在Closure中的實體
Sol 1與Sol 2解決了「儲存在Closure中的實體」的問題:
- 重新定義的函式會失去在重新定義後加上去的屬性
- 建構式與自我重新定義後是不同的
Sol 1
UniverseM.prototype.inNothing = true;
function UniverseM(){
var instance;
UniverseM = function UniverseM(){
return instance;
};
UniverseM.prototype = this;
instance = new UniverseM();
instance.constructor = UniverseM();
instance.start_time = 0;
instance.bang = 'Big';
return instance;
};
var uni7 = new UniverseM();
var uni8 = new UniverseM();
console.log(uni7 === uni8); //true
UniverseM.prototype.inEverything = true;
console.log(uni7.constructor === UniverseM); //true
Sol 2
用立即函式包住建構式和實體,當第一次呼叫的時候,建立一個private物件並回傳;第二次之後的呼叫,只會回傳這份private物件。
var UniverseN;
(function(){
var instance;
UniverseN = function UniverseN(){
if(instance){
return instance;
}
instance = this;
this.start_time = 0;
this.bang = 'Big';
};
}());
var uni9 = new UniverseN();
var uni10 = new UniverseN();
console.log(uni9 === uni10); //true
UniverseN.prototype.inEverything = true;
console.log(uni9.constructor === UniverseN); //true
看程式碼。
總結
總結各方法的實作方式和優缺點:
-
儲存在靜態屬性中的實體
- 實作方法:建立類似
Universe.instance
的屬性,並將物件實體儲存於此。 - 優點:簡單,容易實作。
- 缺點:instance屬性可被public存取,很可能會被修改,而失去應有的正確性。解法:儲存在Closure中的實體。
- 實作方法:建立類似
-
儲存在Closure中的實體
- 實作方法:將物件實體包在一個closure裡面,保持其為private的狀態,使其無法在建構式之外被修改。
- 優點:不會被建構式之外的程式碼修改,具有private特性。
- 缺點:重新定義的函式會失去在重新定義後加上去的屬性,且建構式與自我重新定義後是不同的,這在修改後的Sol 1和Sol 2可以獲得解決。我自己比較喜歡Sol 2,簡短、簡單易懂。
推薦閱讀 / 參考文件
由於部落格搬家了,因此在新落格也放了一份,未來若有增刪會在這裡更新-Singleton 單體模式。
留言