javascript設計模式——單例模式

前面的話

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

相關文章
相關標籤/搜索