[1]標準單例 [2]透明單例 [3]代理實現單例 [4]惰性單例 [5]通用惰性單例 ...
前面的話
單例模式是指保證一個類僅有一個實例,並提供一個訪問它的全局訪問點。 單例模式是一種常用的模式,有一些對象往往只需要一個,比如線程池、全局緩存、瀏覽器中的window對象等。在javaScript開發中,單例模式的用途同樣非常廣泛。試想一下,單擊登錄按鈕時,頁面中會出現一個登錄浮窗,而這個登錄浮窗是唯一的,無論單擊多少次登錄按鈕,這個浮窗都只會被創建一次,那麼這個登錄浮窗就適合用單例模式來創建
標準單例
要實現一個標準的單例模式並不複雜,無非是用一個變數來標誌當前是否已經為某個類創建過對象,如果是,則在下一次獲取該類的實例時,直接返回之前創建的對象。代碼如下:
var Singleton = function( name ){ this.name = name; this.instance = null; }; Singleton.prototype.getName = function(){ alert ( this.name ); }; Singleton.getInstance = function( name ){ if ( !this.instance ){ this.instance = new Singleton( name ); } return this.instance; }; var a = Singleton.getInstance( 'sven1' ); var b = Singleton.getInstance( 'sven2' ); alert ( a === b ); // true
或者:
var Singleton = function( name ){ this.name = name; }; Singleton.prototype.getName = function(){ alert ( this.name ); }; Singleton.getInstance = ( function(){ var instance = null; return function( name ){ if ( !instance ){ instance = new Singleton( name ); } })(); } return instance;
通過Singleton.getInstance來獲取Singleton類的唯一對象,這種方式相對簡單,但有一個問題,就是增加了這個類的“不透明性”,Singleton類的使用者必須知道這是一個單例類,跟以往通過new XXX的方式來獲取對象不同,這裡偏要使用Singleton.getInstance來獲取對象
雖然已經完成了一個單例模式的編寫,但這段單例模式代碼的實際意義並不大
透明單例
現在的目標是實現一個“透明”的單例類,用戶從這個類中創建對象時,可以像使用其他任何普通類一樣。在下麵的例子中,將使用CreateDiv單例類,它的作用是負責在頁面中創建唯一的div節點,代碼如下
var CreateDiv = (function () { var instance; var CreateDiv = function (html) { if (instance) { return instance; } this.html = html; this.init(); return instance = this; }; CreateDiv.prototype.init = function () { var div = document.createElement('div'); div.innerHTML = this.html; document.body.appendChild(div); }; return CreateDiv; })(); var a = new CreateDiv('sven1'); var b = new CreateDiv('sven2'); alert(a === b); // true
雖然現在完成了一個透明的單例類的編寫,但它同樣有一些缺點。為了把instance封裝起來,使用了自執行的匿名函數和閉包,並且讓這個匿名函數返回真正的Singleton構造方法,這增加了一些程式的複雜度,閱讀起來也不是很舒服
上面的代碼中,CreateDiv構造函數實際上負責了兩件事情。第一是創建對象和執行初始化init方法,第二是保證只有一個對象。這是一種不好的做法,至少這個構造函數看起來很奇怪。假設某天需要利用這個類,在頁面中創建千千萬萬的div,即要讓這個類從單例類變成一個普通的可產生多個實例的類,那必須得改寫CreateDiv構造函數,把控制創建唯一對象的那一段去掉,這種修改會帶來不必要的煩惱
代理實現單例
現在通過引入代理類的方式,來解決上面提到的問題。依然使用上面的代碼,首先在CreateDiv構造函數中,把負責管理單例的代碼移除出去,使它成為一個普通的創建div的類
var CreateDiv = function (html) { this.html = html; this.init(); }; CreateDiv.prototype.init = function () { var div = document.createElement('div'); div.innerHTML = this.html; document.body.appendChild(div); }; //引入代理類proxySingletonCreateDiv var ProxySingletonCreateDiv = (function () { var instance; return function (html) { if (!instance) { instance = new CreateDiv(html); } return instance; } })(); var a = new ProxySingletonCreateDiv('sven1'); var b = new ProxySingletonCreateDiv('sven2'); alert(a === b);
通過引入代理類的方式,同樣完成了一個單例模式的編寫,跟之前不同的是,現在把負責管理單例的邏輯移到了代理類proxySingletonCreateDiv中。這樣一來,CreateDiv就變成了一個普通的類,它跟proxySingletonCreateDiv組合起來可以達到單例模式的效果
惰性單例
惰性單例指的是在需要的時候才創建對象實例。惰性單例是單例模式的重點,這種技術在實際開發中非常有用
下麵繼續以登錄框的例子來說明
<button id="loginBtn">登錄</button> <script> var loginLayer = (function () { var div = document.createElement('div'); div.innerHTML = '我是登錄浮窗'; div.style.display = 'none'; document.body.appendChild(div); return div; })(); document.getElementById('loginBtn').onclick = function () { loginLayer.style.display = 'block'; }; </script>
這種方式有一個問題,如果根本不需要進行登錄操作,登錄浮窗一開始就被創建好,很有可能將白白浪費一些 DOM 節點
現在改寫一下代碼,使用戶點擊登錄按鈕的時候才開始創建該浮窗
<button id="loginBtn">登錄</button> <script> var createLoginLayer = function () { var div = document.createElement('div'); div.innerHTML = '我是登錄浮窗'; div.style.display = 'none'; document.body.appendChild(div); return div; }; document.getElementById('loginBtn').onclick = function () { var loginLayer = createLoginLayer(); loginLayer.style.display = 'block'; }; </script>
雖然現在達到了惰性的目的,但失去了單例的效果。每次點擊登錄按鈕時,都會創建一個新的登錄浮窗div
可以用一個變數來判斷是否已經創建過登錄浮窗,代碼如下
var createLoginLayer = (function(){ var div; return function(){ if ( !div ){ div = document.createElement( 'div' ); div.innerHTML = '我是登錄浮窗'; div.style.display = 'none'; document.body.appendChild( div ); } return div; } })(); document.getElementById( 'loginBtn' ).onclick = function(){ var loginLayer = createLoginLayer(); loginLayer.style.display = 'block'; };
上面的代碼仍然存在如下問題:
1、違反單一職責原則的,創建對象和管理單例的邏輯都放在 createLoginLayer對象內部
2、如果下次需要創建頁面中唯一的iframe,或者script標簽,用來跨域請求數據,就必須得如法炮製,把createLoginLayer函數幾乎照抄一遍
var createIframe= (function(){ var iframe; return function(){ if ( !iframe){ iframe= document.createElement( 'iframe' ); iframe.style.display = 'none'; document.body.appendChild( iframe); } return iframe; } })();
通用惰性單例
現在需要把不變的部分隔離出來,先不考慮創建一個div和創建一個iframe有多少差異,管理單例的邏輯其實是完全可以抽象出來的,這個邏輯始終是一樣的:用一個變數來標誌是否創建過對象,如果是,則在下次直接返回這個已經創建好的對象
var obj;
if ( !obj ){
obj = xxx;
}
然後,把如何管理單例的邏輯從原來的代碼中抽離出來,這些邏輯被封裝在getSingle函數內部,創建對象的方法fn被當成參數動態傳入getSingle函數
var getSingle = function( fn ){ var result; return function(){ return result || ( result = fn .apply(this, arguments ) ); } }
接下來將用於創建登錄浮窗的方法用參數fn的形式傳入getSingle,不僅可以傳入createLoginLayer,還能傳入createScript、createIframe、createXhr等。之後再讓getSingle返回一個新的函數,並且用一個變數result來保存fn的計算結果。result變數因為身在閉包中,它永遠不會被銷毀。在將來的請求中,如果result已經被賦值,那麼它將返回這個值
var createLoginLayer = function(){ var div = document.createElement( 'div' ); div.innerHTML = '我是登錄浮窗'; div.style.display = 'none'; document.body.appendChild( div ); return div; }; var createSingleLoginLayer = getSingle( createLoginLayer ); document.getElementById( 'loginBtn' ).onclick = function(){ var loginLayer = createSingleLoginLayer(); loginLayer.style.display = 'block'; };
下麵再試試創建唯一的iframe用於動態載入第三方頁面
var createSingleIframe = getSingle(function () { var iframe = document.createElement('iframe'); document.body.appendChild(iframe); return iframe; }); document.getElementById('loginBtn').onclick = function () { var loginLayer = createSingleIframe(); loginLayer.src = 'https://www.hao123.com'; };
上面的例子中,創建實例對象的職責和管理單例的職責分別放置在兩個方法里,這兩個方法可以獨立變化而互不影響,當它們連接在一起的時候,就完成了創建唯一實例對象的功能
這種單例模式的用途遠不止創建對象,比如通常渲染完頁面中的一個列表之後,接下來要給這個列表綁定click事件,如果是通過ajax動態往列表裡追加數據,在使用事件代理的前提下,click事件實際上只需要在第一次渲染列表的時候被綁定一次,但不想判斷當前是否是第一次渲染列表,如果藉助於jQuery,通常選擇給節點綁定one事件
var bindEvent = function(){ $( 'div' ).one( 'click', function(){ alert ( 'click' ); }); }; var render = function(){ console.log( '開始渲染列表' ); bindEvent(); }; render(); render(); render();
如果利用getSingle函數,也能達到一樣的效果
var getSingle = function (fn) { var result; return function () { return result || (result = fn.apply(this, arguments)); } }; var bindEvent = getSingle(function(){ document.getElementById( 'div1' ).onclick = function(){ alert ( 'click' ); } return true; }); var render = function(){ console.log( '開始渲染列表' ); bindEvent(); }; render(); render(); render();
可以看到,render函數和bindEvent函數都分別執行了3次,但div實際上只被綁定了一個事件