單例模式是指保證一個類僅有一個實例,並提供一個訪問它的全局訪問點。 單例模式是一種經常使用的模式,有一些對象每每只須要一個,好比線程池、全局緩存、瀏覽器中的window對象等。在javaScript開發中,單例模式的用途一樣很是普遍。試想一下,單擊登陸按鈕時,頁面中會出現一個登陸浮窗,而這個登陸浮窗是惟一的,不管單擊多少次登陸按鈕,這個浮窗都只會被建立一次,那麼這個登陸浮窗就適合用單例模式來建立html
要實現一個標準的單例模式並不複雜,無非是用一個變量來標誌當前是否已經爲某個類建立過對象,若是是,則在下一次獲取該類的實例時,直接返回以前建立的對象。代碼以下:java
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
或者:ajax
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構造函數,把控制建立惟一對象的那一段去掉,這種修改會帶來沒必要要的煩惱app
如今經過引入代理類的方式,來解決上面提到的問題。依然使用上面的代碼,首先在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組合起來能夠達到單例模式的效果this
惰性單例指的是在須要的時候才建立對象實例。惰性單例是單例模式的重點,這種技術在實際開發中很是有用
下面繼續以登陸框的例子來講明
<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'; };
上面的代碼仍然存在以下問題:
一、違反單一職責原則的,建立對象和管理單例的邏輯都放在 createLoginLayer對象內部
二、若是下次須要建立頁面中惟一的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實際上只被綁定了一個事件