【學習筆記javascript設計模式與開發實踐(單例模式)----4】

第4章單例模式

 單例模式的定義:保證一個類僅有一個實例,並提供一個訪問它的全局訪問點。

 單例模式是一種常用的模式,有一些對象我們往往只需要一個,比如線程池、全局緩存、瀏覽器的window對象。在js開發中,單例模式的用途同樣非常廣泛。試想一下,當我們單擊登錄按鈕的時候,頁面中會出現一個登錄框,而這個浮窗是唯一的,無論單擊多少次登錄按鈕,這個浮窗只會被創建一次。因此這個登錄浮窗就適合用單例模式。

4.1 實現單便模式

要實現一個標準的單例模式並不複雜,無非是用一個變量來標誌當前是否已經爲某個類創建過對象,如果是,則在下一次獲取該類的實例時,直接返回之前創建的對象:

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 XX的方式來獲取對象不同,這裏偏要使用Singleton.getInstance來獲取對象。

var a = Singleton.getInstance(‘sven1’);
var b = Singleton.getInstance(‘sven2’);
alert(a===b); //true;

4.2 透明的單例模式

我們的目標是實現一個“透明”的單例類,用戶從這個類中創建對象的時候,可以像使用其他任何普通類一樣。在下面的例子中,我們將使用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;
})();

上面的代碼看上去會很難理解,在這段代碼中,CreateDiv的構造函數實際上負責了兩件事情。第一是創建對象和執行初始化的init方法,第二是保證只有一個對象。雖然我們目前我們還沒學習到“單一職責原則”的概念,但可以明確的是,這是一種不好的做法,至少這個構造函數看起來好奇怪……,

假設我們某天需要利用這個類,在頁面上創建多個div,即要讓這個類從單例變成一人普通的可產生多個實例的類,那我們必須改寫CreateDiv構造函數,把控制創建唯一那段去掉,這種修改會給我們帶來不必要的煩惱。

 

4.3 用代理實現單例模式

現在我們通過引入代理方式,來解決上面提到的問題。

首先在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中。這樣一來,CreateDiv就變成了一個普通類,它跟proxySingletonCreateDiv組合起來可以達到單例模式的效果。如下:

<pre name="code" class="javascript">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); //true

4.4 javascript中的單例模式

前端提到的單例模式的實現,更多的是接近傳統面嚮對象語言中的實現,單例對象“類”中創建而來。在以類爲中心的語言中,這是很自然的做法。比如在java中,如果需要某個對象,就必須先定義一個類,對象總是從類中創建而來的。

js其實是一個無類語言,也正因爲如此,生搬單例模式的概念並無意義。在javaScript中創建對象的方法非常簡單,既然我們只需要一個“唯一”的對象,爲什麼要爲它先創建一個類呢(相當有同感)?

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

全局變量不是單例,但在javascript中,我們經常會把全局變量當成單例來使用如:

var a ={};

但這種方式比較糟糕的問題是,全名衝突。維護不容易啊

解決方法有如下兩種:

1.    使用命名空間

適當地使用命名空間,並不會杜絕全局變量,但可以減少全局變量的數量。

如下:

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

把a 和 b都定義爲namespace1的屬性,這樣可以減少變量和全局作用域打交道的機會。另外我們還可以動態地創建命名空間(Object-Oriented javascript)

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[parets[i]];
   }
}
MyApp.namespace(‘event’);
MyApp.namespace(‘dom.style’);
consle.dir(MyApp);
//結果
{
   event:{},
   dom:{
      style:{}
   }
}

2.    全用閉包封裝私有變量

這種方法把一些變量封裝在閉包的內部,只暴露一些接口跟外界通信:

var user =(function(){
var __name=’sven’,
   __age = 29;
  return {
      getUserInfo:function(){
         return __name+’-‘+__age;
      }
  }
})();

4.5 惰性單例

需要才創建,這種技術在實際開發時非常有用,有用的程序超出我們的想象……,如我們在前面所講的Singleton.getInstance的實現。但javascript中並不適用(因爲它是基於類的創建方式生搬硬套感覺在實際應用中真真沒啥用)。

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

Demo Web QQ登錄頁面,當點擊導航的QQ頭像時,會彈出一個登錄浮窗,很明顯這個浮窗在頁面裏總是唯一的,不可能出現同時存在兩個登錄窗口的情況。

第一種解決方案在頁面加載完成的時候便創建好這個div浮窗,這個浮窗一開始肯定是隱藏狀態的,當用戶點擊登錄按鈕的時候,它纔開始顯示

<pre name="code" class="html"><html>
  <body>
    <button id=”loginBtn”>登錄</button>
  </body>
  <script>
   var loginLayer =(function(){
     var div = document.createElement(‘div’);
     div.innerHTML = “登錄浮窗”;
     div.style.display = ‘none’;
     document.body.appendChild(div);
     return div;
   })();
   document.getElementById(‘loginBtn’).οnclick= function(){
     loginLayer.style.display = ‘block’;
   }
  </script>
</html>


這種方式的缺點就是登錄這個頁面,不一定是啓用登錄QQ界面,如我們只是看看天氣,根本不需要進行登錄操作,因爲登錄浮窗總是一開始就被創建好,那麼很有可能將白白浪費一些DOM節點。

那麼我們將其改造一下,

<html>
  <body>
    <button id=”loginBtn”>登錄</button>
  </body>
  <script>
  var createLoginLayer = function(){
    var div = document.createElement(‘div’);
    div.innerHTML = “登錄浮窗”;
    div.style.display = ‘none’;
    document.body.appendChild(div);
    return div;
  };
  document.getElementById(‘loginBtn’).οnclick= function(){
    var loginLayer = createLoginLayer();
    loginLayer.style.display = ‘block’;
  }
  </script>
</html>

在上例中雖然達到惰性的目的,但失去了單例的效果。當我們每次點擊登錄按鈕的時候,都會創建一個新的登錄浮窗div。雖然我們可以在點擊浮窗上的關閉按鈕時把這個浮窗從頁面中刪除,但這樣頻繁地創建和刪除節點明顯是很不合理的,也是不必要的。

所以可以把 createLoginLayer改成單例模式

<pre name="code" class="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’).οnclick= function(){
    var loginLayer = createLoginLayer();
    loginLayer.style.display = ‘block’;
}


4.6 通用的惰性單例

上一節中我們完成的一個可用的惰性單例,但是我們發現它還有如下一些問題。

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

o  如果我們下次需要創建頁面中唯一的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 =xx ; //bala…bala…
}

現在我們將管理單例的邏輯從原來的代碼中抽離出來,這些邏輯被封裝在getSingleton函數內部,創建對象的方法fn被當成參數動態傳入函數:

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’;
  return div;
}
var createSingleLoginLayer =getSingle(createLoginLayer);
document.getElementById(‘loginBtn’).οnclick= 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’).οnclick= function(){
  var loginLayer =createSingleIframe();
  loginLayer.src =‘xxx’;
}

這個例子挺有意思,單例模式的用途不止用於創建對象,比如我們通常渲染完頁面中一個列表之後,接下來要給列表綁定click事件,如果是通過ajax動態往列表裏追回數據,在使用事件代理的前提下,click事件實際上只需要在第一次渲染列表的時候被綁定一次,但是我們不想去判斷當前是否是第一次渲染列表,如果我們是藉助於jQuery,我們通常選擇給節點綁定one事件

var bindEvent = function(){
 $(‘div’).one(“click”,function(){
   alert(‘click’);
 });
};
var render = function(){
  console.log(‘開始渲染列表’);
  bindEvent();
}
render();
render();
render(); //<span style="font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);"> </span>

如果利用getSingle函數,也能達到一樣的效果:

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實際上只被綁定了一個事件。

發佈了78 篇原創文章 · 獲贊 26 · 訪問量 33萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章