單例模式的定義是: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組合起來就能夠達到單例模式的效果;若是單獨使用,就做爲一個普通的類,能產生多個實例對象。閉包
前面提到的單例模式的實現,更多的是接近傳統面嚮對象語言中的實現,單例對象從類中建立而來。在以類爲中心的語言中,這是很天然的作法,好比在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設計模式與開發實踐》第四章 筆記。