在Javascript中,單例模式是一種最基本又經常用到的設計模式,可能在不經意間就用到了單例模式。
本文將從最基礎的理論開始,講述單例模式的基本概念和實現,最後用一個例子來講述單例模式的應用。
理論基礎
概念
單例模式,顧名思義就是只有一個執行個體存在。通過單例模式可以保證系統中一個類只有一個執行個體而且該執行個體易於外界訪問,從而方便對執行個體個數的控制並節約系統資源。如果希望在系統中某個類的對象只能存在一個,單例模式是最好的解決方案。
基本結構
最簡單的單例模式起始就是一個對象字面量,它將有關聯的屬性和方法組織到一起。
var singleton = { prop:"value", method:function(){ }}
這種形式的單例模式,所有成員都是公開的,都可以通過singleton來訪問。這樣的缺點是單例中有一些輔助的方法並不希望暴露給使用者,如果使用者用了這些方法,然後在後面維護的時候,一些輔助方法被刪除,這樣會造成程式錯誤。
如何避免這樣從的錯誤呢?
包含私人成員的單例模式
要怎麼在類中建立私人成員呢,這通過需要閉包來進行實現,關於閉包的知識,本文不再贅述,大家可以自行Google。
基本形式如下:
var singleton = (function () { var privateVar = "private"; return { prop: "value", method: function () { console.log(privateVar); } } })();
首先是一個自執行的匿名函數,在匿名函數中,聲明了一個變數privateVar,返回一個對象賦值給單例對象singleton。在匿名函數外部無法訪問到privateVar變數,它就是單例對象的私人變數,只能在函數內部或通過暴露出來的方法去訪問這個私人變數。這種形式又被成為模組模式。
惰性執行個體化
不管是直接字面量或者私人成員的單例模式,兩者都是在指令碼載入時就被建立出來的單例,但是有時候,頁面可能永遠也用不到這個單例對象,這樣會造成資源浪費。對於這種情況,最佳的處理方式就是惰性載入,就是說在需要的時候才去真正執行個體化這個單例對象,如何?呢?
var singleton = (function () { function init() { var privateVar = "private"; return { prop: "value", method: function () { console.log(privateVar); } } } var instance = null; return { getInstance: function () { if (!instance) { instance = init(); } return instance; } } })();
首先將建立單例對象的代碼封裝到init函數中,然後聲明一個私人變數instance表示單例對象的執行個體,公開一個方法getInstance來擷取單例對象。
調用的時候就通過singleton.getInstance()來進行調用,單例對象是在調用getInstance的時候才真正被建立。
適用場合
單例模式是JS中最常使用的設計模式,從增強模組性和程式碼群組織性等方面來說,應該儘可能的使用單例模式。它可以把相關程式碼群組織到一起便於維護,對於大型項目,每個模組惰性載入可以提高效能,隱藏實現細節,暴露出常用的api。常見的類庫比如underscore,jQuery我們都可以將其理解為單例模式的應用。
結合實戰
前面已經講過,單例模式是最常用的設計模式之一,我們來舉個例子進行說明,
下面的代碼主要實現一個簡單的日期協助類,通過單例模式實現:
基本的單例模式結構
var dateTimeHelper = { now: function () { return new Date(); }, format: function (date) { return date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate(); } }; console.log(dateTimeHelper.now());
這段代碼通過對象字面量實現單例模式,使用的時候直接調用方法即可。
惰性載入實現單例模式
var dateTimeHelper = (function () { function init() { return { now: function () { return new Date(); }, format: function (date) { return date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate(); } } } var instance = null; return { getInstance: function () { if (!instance) { instance = init(); } return instance; } } })(); console.log(dateTimeHelper.getInstance().now())
這就是惰性載入的單例模式。
下面再來看幾個執行個體:
實現1: 最簡單的對象字面量
var singleton = { attr : 1, method : function(){ return this.attr; } }var t1 = singleton ;var t2 = singleton ;
那麼很顯然的, t1 === t2 。
十分簡單,並且非常使用,不足之處在於沒有什麼封裝性,所有的屬性方法都是暴露的。對於一些需要使用私人變數的情況就顯得心有餘而力不足了。當然在對於 this 的問題上也是有一定弊端的。
實現2:建構函式內部判斷
其實和最初的JS實現有點類似,不過是將對是否已經存在該類的執行個體的判斷放入建構函式內部。
function Construct(){ // 確保只有單例 if( Construct.unique !== undefined ){ return Construct.unique; } // 其他代碼 this.name = "NYF"; this.age="24"; Construct.unique = this;}var t1 = new Construct() ;var t2 = new Construct() ;
那麼也有的, t1 === t2 。
也是非常簡單,無非就是提出一個屬性來做判斷,但是該方式也沒有安全性,一旦我在外部修改了Construct的unique屬性,那麼單例模式也就被破壞了。
實現3 : 閉包方式
對於大著 靈活 牌子的JS來說,任何問題都能找到 n 種答案,只不過讓我自己去掂量孰優孰劣而已,下面就簡單的舉幾個使用閉包實現單例模式的方法,無非也就是將建立了的單例緩衝而已。
var single = (function(){ var unique; function Construct(){ // ... 產生單例的建構函式的代碼 } unique = new Constuct(); return unique;})();
只要 每次講 var t1 = single; var t2 = single;即可。 與對象字面量方式類似。不過相對而言更安全一點,當然也不是絕對安全。
如果希望會用調用 single() 方式來使用,那麼也只需要將內部的 return 改為
return function(){ return unique; }
以上方式也可以使用 new 的方式來進行(形式主義的趕腳)。當然這邊只是給了閉包的一種例子而已,也可以在 Construct 中判斷單例是否存在 等等。 各種方式在各個不同情況做好選著即可。
總結
單例模式的好處在於對代碼的組織作用,將相關的屬性和方法封裝在一個不會被多次執行個體化的對象中,讓代碼的維護和調試更加輕鬆。隱藏了實現細節,可以防止被錯誤修改,還防止了全域命名空間的汙染。另外可以通過惰性載入提高效能,減少不必要的記憶體消耗。