1. 程式人生 > >[轉]js設計模式-單例模式

[轉]js設計模式-單例模式

bsp append 線程池 get 有一個 layer 代理 分享圖片 獨立

 單例模式是指保證一個類僅有一個實例,並提供一個訪問它的全局訪問點。 單例模式是一種常用的模式,有一些對象往往只需要一個,比如線程池、全局緩存、瀏覽器中的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實際上只被綁定了一個事件

[轉]js設計模式-單例模式