1. 程式人生 > >JavaScript設計模式 -- 單例模式

JavaScript設計模式 -- 單例模式


作者: DocWhite白先生

一.概念

單例模式的定義是保證一個類僅有一個例項,並提供一個訪問它的全域性訪問點。
單例模式是一種常用的模式,例如執行緒池、全域性快取、瀏覽器中的window物件等。
下面是一個簡單例子:

function singleton(name) {
	this.name = name;
	this.instance = null;
}
singleton.prototype.getName = function() {
	return this.name;
}
singleton.getInstance = function(name) {
	return this.instance === null? new singleton(name) : this.instance;
}
const alex = singleton.getInstance('alex');
const john = singleton.getInstance('john');
console.log(alex === john); 			// true

這個簡單的單例模式存在一個問題,就是這個單例類的使用者必須知道它是單例類,跟通過new XXX的方式獲取物件不同,這裡偏要使用singleton.getInstance來獲取單例例項。

所以這時候需要一個透明的單例模式,可以像使用其他任何類一樣從這個類中建立物件的時候返回單例物件。

二. 透明的單例模式

要想用new XXX的形式建立單例模式的例項,只需要巧用立即執行函式。

const singleton = (function() {
	let instance = null;
	const __singleton = function(name) {
		if(instance){
			return instance;
		}
		this.name = name;
		return instance = this;
	}
	return  __singleton;
}) ()

const alex = new singleton('alex');
const john = new singleton('john');
console.log(alex === john);			//  true

雖然這樣實現了透明的單例模式,以讓我們能夠以new 命令建立單例,但是立即執行函式返回一個真正的__singleton構造方法,這增加了一些程式複雜性,同時該構造方法實際上負責了兩件事情,第一個是初始化例項屬性name,二是保證只有一個物件,雖然沒有接觸過“單一職責原則”的概念,但這是一種不好的做法。
假設未來需要把這個單例類變成一個可以產生多個例項的類,那我們必須改寫__singleton這個建構函式,把控制建立唯一物件的那段去掉。
有沒有更好的實現方式呢?有!

三. 用代理實現單例模式

修改一下上一個例子中的程式碼

const singleton = function(name) {
	this.name = name;
}
const SingletonProxy = (function() {
	let instance;
	return function(name) {
		if(!instance){
			instance = new singleton(name);
		}
		return instance;
	}
})();
const alex = new SingletonProxy('alex');
const john = new SingletonProxy('john');
console.log(alex === john); 			//  true

通過引入代理類的方式,我們同樣完成了一個單例模式的編寫,跟之前不同的是,現在我們 把負責管理單例的邏輯移到了代理類 SingletonProxy 中。這樣一來,_singleton 就變成了 一個普通的類。它跟SingletonProxy配合起來使用才達到單例模式的效果。

四. Javascript 中的單例模式。

JavaScript跟傳統的面嚮物件語言相比,JavaScript是一門無類語言(class-free),既然我們知道單例物件是從單例“類”中建立而來,這在傳統面嚮物件語言中,是很自然的,但是JavaScript中建立物件是一件非常簡單的事情,既然我們需要一個“唯一”物件,為什麼要先建立“類”,所以實質上在JavaScript中,我們常通過建立一個全域性變數當做單例來使用。

//在 this === window  的環境下
var singleton = {};

這個singleton變數實際上已經滿足了單例模式的兩個條件,可以程式碼的任何位置使用這個變數,以及提供了一個全域性訪問的方法(當然)。但是全域性變數存在一個問題,這會導致記憶體洩漏、名稱空間汙染等問題,而且 變數很容易會被別人覆蓋。
即使是JavaScript的設計者本人Brandan Eich也承認全域性變數是設計上的失誤,是在沒有足夠時間思考一些東西的情況下導致的結果。

作為開發者,我們可以使用以下幾個方法降低或減少全域性變數的使用,即使使用它也要把它的汙染降到最低。

1. 使用名稱空間

var myObject = {
	namespace: 'namespace1',
	name: null,
	a: function() {
		console.log(this.namespace)
	},
	b: function(name) {
		this.name = name;
	}
}

把a、b函式定義為myObject的屬性,這樣就人為的減少了a、b和全域性變數打交道的機會。

2. 用閉包封裝私有變數

這種方法把一些變數封裝在閉包的內部, 只暴露一些介面跟外界通訊:

var user = (function() {
	var __name = 'alex', __age = 24;
	return {
		getUserInfo: function() {
			return __name + '-' + __age;
		}
	}
})();
// __name, __age被封裝在閉包產生的作用域中,外部無法訪問。避免了對全域性的命令汙染。

3. 惰性單例

惰性單例指的是在需要的時候才建立物件例項。惰性單例是單例模式的重點,這種技術在實 際開發中非常有用,有用的程度可能超出了我們的想象,實際上在本章開頭就使用過這種技術, instance 例項物件總是在我們呼叫 singleton.getInstance 的時候才被建立,而不是在頁面載入好 的時候就建立。
但是這是基於“類”的單例模式,前面說過基於“類”的單例模式在JavaScript中並不適用。
以一個全域性彈窗例子作為示例:

var createModal = function(children) {
	var div = document.createElement('div');
	if(children){
		div.appendChild(children);
	}
	div.style.display = 'none';
	document.body.appendChild(div);
	return div;
}
document.getElementById('openModal').addEventListener('click', function() {
	var children = document.createElement('h1');
	children.innerHTML = 'this is modal children!';
	var modal = createModal(children);
	modal.style.display = 'block';
});

雖然實現了惰性的目的,但是上面這個例子失去了單例的效果,總不能每次點選開啟彈窗的按鈕都建立一次單床,關閉的時候刪除,這樣頻繁的建立和刪除節點明顯不合適,也是不必要的。
這時候可以用一個變數來判斷是否已經建立過彈窗,這也是文章一開始第一段程式碼使用的方法,改造一下createModal方法:

var createModal = (function() {
	var modal = null;
	return function(children) {
		if(!div){
			div = document.createElement('div');
			if(children){
				div.appendChild(children);
			}
			div.style.display = 'none';
			document.body.appendChild(div);
		}
		return div;
	}
})()

4. 通用的單例

上面的例子雖然完成了一個可用的惰性單例,但是還有一些問題:

  1. 這段程式碼違反了“單一職責原則”,建立物件和管理單例都在createModal物件內部。
  2. 如果下次需要建立的不再是彈窗,而是iframe或者img等,相結合如法炮製把createModal照抄一遍。

這時候就需要把不變的部分隔離出來,先不考慮建立一個div和建立img有多少差異,管理單例的邏輯完全可以抽象出來,並且這個邏輯始終是一樣的:用一個變數去標識是否建立過對線個,如果是則在下次直接返回這個已經建立過的物件:

var getSingleton = function() {
	var instance;
	return function(fn) {
		return instance || ( instance = fn.apple( this, arguments ) );
	}
}
// 接下來將用於建立彈窗的方法用引數fn的形式傳入getSingleton,這樣就可以通過動態的傳fn調整這個單例的適用場景
var createModal = function(children) { 
	var div = document.createElement('div');
	if( children ){  div.appendChild(children);  }
	div.style.display = 'none';
	document.body.appendChild(div);
	return div;
}

var createSingleModal = getSingleton(createModal);

document.getElementById('openModal').addEventListener('click', function() {
	var h1 = document.createElement('h1');
	h1.innerHTML = "I'm Modal!";
	var modal = createSingleModal(h1);
	modal.style.display = ’block‘;
})

這種單例模式的用途遠不止建立物件,比如我們通常渲染完頁面中的一個列表之後,接下來 9 要給這個列表繫結 click 事件,如果是通過 ajax 動態往列表裡追加資料,在使用事件代理的前提
下,click 事件實際上只需要在第一次渲染列表的時候被繫結一次,但是我們不想去判斷當前是 否是第一次渲染列表。

結語

單例模式是一種簡單但非常實 用的模式,特別是惰性單例技術,在合適的時候才建立物件,並且只建立唯一的一個。更奇妙的 是,建立物件和管理單例的職責被分佈在兩個不同的方法中,這兩個方法組合起來才具有單例模 式的威力。