JavaScript設計模式——單例模式

本文由《JavaScript設計模式與開發實踐》–曾探著總結而來。

應用場景

某個作用域內(如全局作用域)只需要一個具備功能的對象。例如全局的window,登錄框等等。總之,就是在頂級的作用域中用來掌控頂級信息的對象。這樣的對象是唯一的,適合用單例模式來創建。

創建方式

思路:包括兩點,

  1. 首先要創建這樣的對象。創建對象可以用ES6語法中的類(或ES5的構造函數),通過new 類名()來創建,也可以用對象字面量語法創建。
  2. 其次要保證對象唯一。保證唯一可以採用只調用一次類的構造函數,再次調用直接返回已創建好的對象。當然,對象字面量語法創建的對象本身就是唯一的。

方式一:getInstance()方法返回單例

利用類來創建,但不通過new方式調用,而是調用專門創建單例的方法。

// 例如創建一個存儲數據的Store對象
// 在類上掛載靜態屬性來區分實例是否創建
class Store {
	constructor(props) {}
	Store.instance = null; // 在類上掛載靜態屬性instance來判斷是否已經創建了實例
	Store.getInstance = function(props) {
		if(!this.instance) {
			this.instance = new Store(props);
		}
		return this.instance;
	}
}
var store1 = Store.getInstance(props1);
var store2 = Store.getInstance(props2);
store1 === store2; // true
// 同樣可以通過閉包變量來區分實例是否創建
class Store {
	constructor(props) {}
	Store.getInstance = (function() {
	// 聲明一個閉包變量,用來判斷實例是否已經創建
		var instance = null;
		return function(props) {
			if(!instance) {
				instance = new Store(props);
			}
			return instance;
		}
	})()
}

這種方式在思路上很清晰,但卻存在着“不透明”的問題。使用的是Store.getInstance()來創建實例,而並非通用的new調用方式。另外,創建對象和保證唯一兩種功能耦合在一起,不符合“函數單一職責”的原則,不利於代碼複用和擴展。

方式二:new調用返回單例

使用類來創建,但用new調用,符合通常的創建實例方式。既然是用new調用,那麼創建實例和判斷唯一性就會在構造函數中進行。

class Store {
	Store.instance = null;
	constructor(props) {
		if(Store.instance) {
			return Store.instance;
		}
		// 執行一系列初始化this的邏輯;
		Store.instance = this;
		return Store.instance;
	}
}

這種方式解決了“透明”問題,但創建實例和保證唯一仍然耦合在一起。

方式三:代理模式,分離創建實例和保證唯一的耦合,將保證唯一的功能集成到代理函數中

利用代理模式,將創建實例的功能和保證唯一的功能分離開,各自集成在不同的函數中,從而可以使代碼利於複用。這樣創建實例的時候可以按正常方式創建很多實例,當需要創建單例時,用代理函數調用即可。

// 創建實例的功能由Store負責,保證唯一的功能由getStoreInstance函數負責;
class Store {
	constructor(props) {}
}
// getStoreInstance函數負責代理保證唯一的功能;
const getStoreInstance = (function() {
	var instance = null;
	return function(props) {
		if(!instance) {
			intance = new Store(props);
		}
		return instance;
	}
})()

對象字面量語法創建單例

對象字面量語法創建單例很方便,但容易污染全局作用域。由兩種方式來減少污染:

  • 把單例置於命名空間中
  • 用閉包將單例變爲私有變量

管理單例的邏輯

由上述代碼發現,管理單例的邏輯(即如何保證唯一的邏輯)基本上類似於如下代碼:

var instance = null;
if(!intance) {
	// 創建實例並賦予instance
}
return instance;

惰性單例

有的時候不需要頁面加載時就創建單例,而是響應用戶行爲,或者在頁面空閒時創建單例,這樣會使頁面性能提高。例如,登錄框的創建,用戶可能並不想登錄,只是想瀏覽頁面,此時的登錄框適合用惰性單例的方式去創建,在用戶點擊登錄按鈕時創建登錄框。

// html
<body>
	<button id="login">登錄</button>
</body>
// script
<script>
// getInstance函數用來代理單例的管理,接收任何創建對象的方法,返回該單例對象。這樣不管創建什麼對象,都可以用getInstance函數來管理單例。
	const getInstance = (function() {
		var instance = null;
		// fn爲創建DOM節點的函數,但不能是通過new調用的實例化函數;
		return function(fn) {
			var args = Array.prototype.slice.apply(arguments, 1); // args保存除fn外的其他參數;
			if(!instance) {
				instance = fn.apply(this, args);
			}
			return instance;
		}
	})()
	const createLogin = function() {
		var div = document.createElement('div');
		div.innerHTML = '請登錄';
		div.style.display = 'none';
		document.body.appendChild(div);
		return div;
	}
	document.getElementById('login').addEventListener('click', function() {
		var div = getInstance(createLogin);
		div.style.display = 'block';
	});
</script>

單例模式的其他應用

在單例模式的管理邏輯中,實際上不只是創建單例對象,所有只需要執行一次的行爲邏輯,都可以利用單例模式來管理。如jQuery中只綁定一次事件的one()方法,即可以用單例模式去實現。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章