JavaScript設計模式與開發實踐 | 04 - 單例模式

單例模式

單例模式的定義是:javascript

保證一個類僅有一個實例,並提供一個訪問它的全局訪問點。html

單例模式是一種經常使用的模式,有一些對象咱們每每只須要一個,好比線程池、全局緩存、瀏覽器的window對象等。例如,當咱們點擊登陸按鈕時,頁面會彈出一個登陸懸浮窗,而這個登陸懸浮窗是惟一的,不管點擊多少次登陸按鈕,這個懸浮窗只會被建立一次,這時,這個懸浮窗就適合用單例模式來建立。java

實現單例模式

實現一個標準的單例模式,通常是用一個變量來標誌當前是否已經爲某個類建立過對象,如果,則在下一次獲取該類的實例時,直接返回以前建立的對象。web

不透明的單例模式

var Singleton = function(name){
  this.name = name;
  this.instance = null;
}

Singleton.prototype.getName = function(){
  console.log(this.name);
};

Singleton.getInstance = function(name){
  if(! this.instance){
      this.instance  = new Singleton(name);
  }
  return this.instance;
};

var a = Singleton.getInstance('sin1');
var b = Singleton.getInstance('sin2');

console.log(a === b);  // 輸出:true

咱們經過Singleton.getInstance來獲取Singleton類的惟一對象,這種方式想對簡單,但有一個問題,就是增長了類的「不透明性」,Singleton類的使用者必須知道這是一個單例類,跟以往經過new xxx來獲取對象的方式不一樣,這裏只能使用Singleton.getInstance來獲取對象。ajax

透明的單例模式

如今咱們經過一段代碼來實現一個透明的單例類,用戶從這個類中建立對象的時候,能夠像使用其餘任何普通類同樣。編程

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('sin1');
var b = new createDiv('sin2');

console.log(a === b);  // 輸出:true

爲了把instance封裝起來,咱們使用了自執行的匿名函數和閉包,而且讓這個匿名函數返回真正的Singleton構造方法,這增長了一些程序的複雜度,閱讀起來也不是很舒服。設計模式

觀察Singleton構造函數的代碼,該構造函數實際上負責了兩件事情:第一是建立對象和執行初始化init方法,第二是保證只有一個對象。這不符合設計原則中的「單一職責原則」,這是一種很差的作法。假設咱們某天須要利用這個類,在頁面中建立不少個div,即讓這個類從單例類編程一個普通的能夠產生多個實例的類,咱們就得改寫createDiv構造函數,把控制建立惟一對象的那一段去掉,這種修改會給咱們帶來沒必要要的煩惱。瀏覽器

用代理實現單例模式

如今咱們經過引入代理類的方法,來解決上面提到的問題。緩存

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('sin1');
var b = new proxySingletonCreateDiv('sin2');

咱們把負責管理單例的邏輯移到了代理類proxySingletonCreateDiv中。這樣一來,createDiv就變成了一個普通的類,它跟proxySingletonCreateDiv組合起來就能夠達到單例模式的效果;若是單獨使用,就做爲一個普通的類,能產生多個實例對象。閉包

JavaScript中的單例模式

前面提到的單例模式的實現,更多的是接近傳統面嚮對象語言中的實現,單例對象從類中建立而來。在以類爲中心的語言中,這是很天然的作法,好比在Java中,若是須要某個對象,就必須先定義一個類,對象老是從類中建立而來。

但JavaScript是一門無類語言,生搬單例模式的概念並沒有意義。在JavaScript中建立對象很是簡單,直接聲明便可。既然這樣,咱們就沒有必要爲它先建立一個類。

單例模式的核心是確保只有一個實例,並提供全局訪問。

全局變量不是單例模式,但在JavaScript開發中,咱們常常會把全局變量當成單例模式來使用,例如var a = {};

當用這種方式建立對象a時,對象a確實獨一無二。若是變量a被聲明在全局做用域下,則咱們能夠在代碼中的任何位置使用這個變量,全局變量天然能全局訪問。這樣就知足了單例模式的兩個條件。

可是,全局變量存在一些問題:

  • 容易形成命名空間污染;

  • 在大型項目中,若是不加以限制和管理,程序中可能存在不少這樣的變量;

  • JavaScript中的變量很容易被不當心覆蓋。

所以,在使用全局變量時,咱們要盡力下降它的污染,經過如下方式:

1.使用命名空間
適當地使用命名空間,並不會杜絕全局變量,但能夠減小全局變量的數量。
最簡單的方法依然是用對象字面量的方式:

var namespace1 = {
  a: function(){
      alert(1);
  },
  b: function(){
      alert(2);
  }
};

把a和b都定義爲namespace1的屬性,這樣能夠減小變量和全局做用域打交道的機會。‘

另外,能夠動態地建立命名空間,如:

var myApp = {};

myApp.namespace = function(name){
  var parts = name.split('.');
  var current = myApp;
  for(var i in parts){
      if(!current[parts[i]]){
          current[parts[i]] = {};
      }
      current = current[parts[i]];
  }
};

myApp.namespace('event');
myApp.namespace('dom.style');

上述代碼等價於:

var myApp = {
  event:{},
  dom:{
    style:{}
  }
};

2.使用閉包封裝私有變量
這種方法把一些變量封裝在閉包的內部,只暴露一些接口跟外界通訊:

var user = (function(){
  var __name = 'sin1';
  var __age = 29;

  return {
      getUserInfo: function(){
          return __name + '-' + __age;
      }
  }
})();

咱們用下劃線來約定私有變量__name和__age,它們被封裝在閉包產生的做用域中,外部是訪問不到這兩個變量的,這就避免了對全局的命名污染。

惰性單例

惰性單例指的是在須要的時候才建立對象實例。惰性單例在實際開發中很是有用,是單例模式的重點。

咱們在開頭寫的Singleton類就用過這種技術,instance實例對象老是在咱們調用Singleton.getInstance的時候才被建立,而不是在頁面加載好的時候就建立。

實現惰性單例

假設,在一個提供登陸功能(點擊登陸按鈕彈出一個登陸懸浮窗)的web頁面中,可能用戶在訪問過程當中,根本不須要進行登陸操做,只須要瀏覽某些內容。因此,沒有必要在頁面加載好以後就立刻建立登陸懸浮窗,只須要當用戶點擊登陸按鈕的時候纔開始建立登陸懸浮窗,實現代碼以下:

<!DOCTYPE html>
<html>
<head>
  <title>惰性單例</title>
</head>
<body>
  <button id = "loginBtn">登陸</button>
</body>

<script type="text/javascript">
  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';
  };
</script>

</html>

但這段代碼仍是存在一些問題的:

  • 這段代碼仍然是違反單一職責原則的,建立對象和管理單例的邏輯都放在createLoginLayer對象內部;

  • 若是咱們下次須要建立頁面中惟一的iframe,或者script標籤,必須得如法炮製,把createLoginLayer函數幾乎照抄一遍。

通用的惰性單例

爲了解決上面的問題,咱們能夠實現一段通用的惰性單例代碼:

<!DOCTYPE html>
<html>
<head>
  <title>惰性單例</title>
</head>
<body>
  <button id = "loginBtn">登陸</button>
</body>

<script type="text/javascript">
  var getSingle = function(fn){
      var result;
      return function(){
        return result || (result = fn.apply(this, arguments));
      }
  };

  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 = 'http://baidu.com';
  };
</script>

</html>

上面的代碼,

  • 把管理單例的邏輯抽象了出來:用一個變量來標誌是否建立過對象,若是是,則在下次直接返回這個已經建立好的對象;

  • 把如何管理單例的邏輯封裝在getSingle函數內部,建立對象的方法fn被當成參數動態傳入getSingle函數;

  • 將建立登陸懸浮窗的方法傳入getSingle,還能傳入createIframe,createScript;

  • getSingle函數返回一個新的函數,而且用一個變量result來保存fn的計算結果,result變量在閉包中,永遠不會被銷燬,因此在未來的請求中,若是result已經被賦值,那麼它將返回這個值。

單例模式的用途不止在於建立對象,好比咱們一般渲染完頁面中的一個列表後,就要給這個列表綁定click事件,若是經過ajax動態往列表裏追加數據,在使用事件代理的前提下,click事件實際上只須要在第一次渲染列表的時候就被綁定一次。

<!DOCTYPE html>
<html>
<head>
  <title>惰性單例</title>
</head>
<body>
  <button id = "renderBtn">渲染列表</button>
</body>

<script type="text/javascript">
  var getSingle = function(fn){
      var result;
      return function(){
        return result || (result = fn.apply(this, arguments));
      }
  };

  var bindEvent = getSingle(function(){
      console.log('綁定click事件');
      document.getElementById('renderBtn').onclick = function(){
          alert('click');
      }
      return true;
  });

  var render = function(){
      console.log('開始渲染');
      bindEvent();
  }

  render();
  render();
  render();

  // 最終輸出結果:
  // 開始渲染
  // 綁定click事件
  // 開始渲染
  // 開始渲染
</script>

</html>

PS:本節內容爲《JavaScript設計模式與開發實踐》第四章 筆記。

相關文章
相關標籤/搜索