[轉] JavaScript 單例模式
定義
確保一個類僅有一個實例,並提供一個訪問它的全局訪問點。
單例模式使用的場景
比如線程池、全局緩存等。我們所熟知的瀏覽器的window對象就是一個單例,在JavaScript開發中,對於這種只需要一個的對象,我們的實現往往使用單例。
實現單例模式 (不透明的)
一般我們是這樣實現單例的,用一個變量來標誌當前的類已經創建過對象,如果下次獲取當前類的實例時,直接返回之前創建的對象即可。代碼如下:
// 定義一個類
function Singleton(name) {
this.name = name;
this.instance = null;
}
// 原型擴展類的一個方法getName()
Singleton.prototype.getName = function() {
console.log(this.name)
};
// 獲取類的實例
Singleton.getInstance = function(name) {
if(!this.instance) {
this.instance = new Singleton(name);
}
return this.instance
};
// 獲取對象1
var a = Singleton.getInstance(‘a‘);
// 獲取對象2
var b = Singleton.getInstance(‘b‘);
// 進行比較
console.log(a === b);
我們也可以使用閉包來實現:
function Singleton(name) {
this.name = name;
}
// 原型擴展類的一個方法getName()
Singleton.prototype.getName = function() {
console.log(this.name)
};
// 獲取類的實例
Singleton.getInstance = (function() {
var instance = null;
return function(name) {
if(!this.instance) {
this.instance = new Singleton(name);
}
return this.instance
}
})();
// 獲取對象1
var a = Singleton.getInstance(‘a‘);
// 獲取對象2
var b = Singleton.getInstance(‘b‘);
// 進行比較
console.log(a === b);
這個單例實現獲取對象的方式經常見於新手的寫法,這種方式獲取對象雖然簡單,但是這種實現方式不透明。知道的人可以通過 Singleton.getInstance()
獲取對象,不知道的需要研究代碼的實現,這樣不好。這與我們常見的用 new
關鍵字來獲取對象有出入,實際意義不大。
實現單例模式 (透明的)
var Singleton = (function(){
var instance;
var CreateSingleton = function (name) {
this.name = name;
if(instance) {
return instance;
}
// 打印實例名字
this.getName();
// instance = this;
// return instance;
return instance = this;
}
// 獲取實例的名字
CreateSingleton.prototype.getName = function() {
console.log(this.name)
}
return CreateSingleton;
})();
// 創建實例對象1
var a = new Singleton(‘a‘);
// 創建實例對象2
var b = new Singleton(‘b‘);
console.log(a===b);
這種單例模式我以前用過一次,但是使用起來很別扭,我也見過別人用這種方式實現過走馬燈的效果,因為走馬燈在我們的應用中絕大多數只有一個。
這裏先說一下為什麽感覺不對勁,因為在這個單例的構造函數中一共幹了兩件事,一個是創建對象並打印實例名字,另一個是保證只有一個實例對象。這樣代碼量大的化不方便管理,應該盡量做到職責單一。
我們通常會將代碼改成下面這個樣子:
// 單例構造函數
function CreateSingleton (name) {
this.name = name;
this.getName();
};
// 獲取實例的名字
CreateSingleton.prototype.getName = function() {
console.log(this.name)
};
// 單例對象
var Singleton = (function(){
var instance;
return function (name) {
if(!instance) {
instance = new CreateSingleton(name);
}
return instance;
}
})();
// 創建實例對象1
var a = new Singleton(‘a‘);
// 創建實例對象2
var b = new Singleton(‘b‘);
console.log(a===b);
這種實現方式我們就比較熟悉了,我們在開發中經常會使用中間類,通過它來實現原類所不具有的特殊功能。有的人把這種實現方式叫做代理,這的確是單例模式的一種應用,稍後將在代理模式進行詳解。
說了這麽多我們還是在圍繞著傳統的單例模式實現在進行講解,那麽具有JavaScript特色的單例模式是什麽呢。
JavaScript單例模式
在我們的開發中,很多同學可能並不知道單例到底是什麽,應該如何使用單例,但是他們所寫的代碼卻剛好滿足了單例模式的要求。如要實現一個登陸彈窗,不管那個頁面或者在頁面的那個地方單擊登陸按鈕,都會彈出登錄窗。一些同學就會寫一個全局的對象來實現登陸窗口功能,是的,這樣的確可以實現所要求的登陸效果,也符合單例模式的要求,但是這種實現其實是一個巧合,或者一個美麗的錯誤。由於全局對象,或者說全局變量正好符合單例的能夠全局訪問,而且是唯一的。但是我們都知道,全局變量是可以被覆蓋的,特別是對於初級開發人員來說,剛開始不管定義什麽基本都是全局的,這樣的好處是方便訪問,壞處是一不留意就會引起沖突,特別是在做一個團隊合作的大項目時,所以成熟的有經驗的開發人員盡量減少全局的聲明。
而在開發中我們避免全局變量汙染的通常做法如下:
- 全局命名空間
- 使用閉包
它們的共同點是都可以定義自己的成員、存儲數據。區別是全局命名空間的所有方法和屬性都是公共的,而閉包可以實現方法和屬性的私有化。
惰性單例模式
說實話,在我下決心學習設計模式之前我並不知道,單例模式還分惰性單例模式,直到我看了曾探大神的《JvaScript設計模式與開發實踐》後才知道了還有惰性單例模式,那麽什麽是惰性單例模式呢?在說惰性單例模式之前,請允許我先說一個我們都知道的lazyload加載圖片,它就是惰性加載,只當含有圖片資源的dom元素出現在媒體設備的可視區時,圖片資源才會被加載,這種加載模式就是惰性加載;還有就是下拉刷新資源也是惰性加載,當你觸發下拉刷新事件資源才會被加載等。而惰性單例模式的原理也是這樣的,只有當觸發創建實例對象時,實例對象才會被創建。這樣的實例對象創建方式在開發中很有必要的。
就如同我們剛開始介紹的用 Singleton.getInstance
創建實例對象一樣,雖然這種方式實現了惰性單例,但是正如我們剛開始說的那樣這並不是一個好的實現方式。下面就來介紹一個好的實現方式。
遮罩層相信大家對它都不陌生。它在開發中比較常見,實現起來也比較簡單。在每個人的開發中實現的方式不盡相同。這個最好的實現方式還是用單例模式。有的人實現直接在頁面中加入一個div然後設置display為none,這樣不管我們是否使用遮罩層頁面都會加載這個div,如果是多個頁面就是多個div的開銷;也有的人使用js創建一個div,當需要時就用將其加入到body中,如果不需要就刪除,這樣頻繁地操作dom對頁面的性能也是一種消耗;還有的人是在前一種的基礎上用一個標識符來判斷,當遮罩層是第一次出現就向頁面添加,不需要時隱藏,如果不是就是用前一次的添加的。
實現代碼如下:
// html
<button id="btn">click it</button>
// js
var createMask = (function() {
var mask;
return function() {
if(!mask) {
// 創建div元素
var mask = document.createElement(‘div‘);
// 設置樣式
mask.style.position = ‘fixed‘;
mask.style.top = ‘0‘;
mask.style.right = ‘0‘;
mask.style.bottom = ‘0‘;
mask.style.left = ‘0‘;
mask.style.opacity = ‘‘;
mask.style.display = ‘none‘;
document.body.appendChild(mask);
}
return mask;
}
})();
document.getElementById(‘btn‘).onclick = function() {
var maskLayer = createMask();
maskLayer.style.display = ‘block‘;
}
我們發現在開發中並不會單獨使用遮罩層,遮罩層和彈出窗是經常結合在一起使用,前面我們提到過登陸彈窗使用單例模式實現也是最適合的。那麽我們是不是要將上面的代碼拷貝一份呢?當然我們還有好的實現方式,那就是將上面單例中代碼變化的部分和不變的部分,分離開來。
代碼如下:
var singleton = function(fn) {
var instance;
return function() {
return instance || (instance = fn.apply(this, arguments));
}
};
// 創建遮罩層
var createMask = function(){
// 創建div元素
var mask = document.createElement(‘div‘);
// 設置樣式
mask.style.position = ‘fixed‘;
mask.style.top = ‘0‘;
mask.style.right = ‘0‘;
mask.style.bottom = ‘0‘;
mask.style.left = ‘0‘;
mask.style.opacity = ‘o.75‘;
mask.style.backgroundColor = ‘#000‘;
mask.style.display = ‘none‘;
mask.style.zIndex = ‘98‘;
document.body.appendChild(mask);
// 單擊隱藏遮罩層
mask.onclick = function(){
this.style.display = ‘none‘;
}
return mask;
};
// 創建登陸窗口
var createLogin = function() {
// 創建div元素
var login = document.createElement(‘div‘);
// 設置樣式
login.style.position = ‘fixed‘;
login.style.top = ‘50%‘;
login.style.left = ‘50%‘;
login.style.zIndex = ‘100‘;
login.style.display = ‘none‘;
login.style.padding = ‘50px 80px‘;
login.style.backgroundColor = ‘#fff‘;
login.style.border = ‘1px solid #ccc‘;
login.style.borderRadius = ‘6px‘;
login.innerHTML = ‘login it‘;
document.body.appendChild(login);
return login;
};
document.getElementById(‘btn‘).onclick = function() {
var oMask = singleton(createMask)();
oMask.style.display = ‘block‘;
var oLogin = singleton(createLogin)();
oLogin.style.display = ‘block‘;
var w = parseInt(oLogin.clientWidth);
var h = parseInt(oLogin.clientHeight);
}
在上面的實現中將單例模式的惰性實現部分提取出來,實現了惰性實現代碼的復用,其中使用apply改變改變了fn內的this指向,使用 ||
預算簡化代碼的書寫。
[轉] JavaScript 單例模式