本文由《JavaScript設計模式與開發實踐》–曾探著總結而來。
應用場景
某個作用域內(如全局作用域)只需要一個具備功能的對象。例如全局的window,登錄框等等。總之,就是在頂級的作用域中用來掌控頂級信息的對象。這樣的對象是唯一的,適合用單例模式來創建。
創建方式
思路:包括兩點,
- 首先要創建這樣的對象。創建對象可以用ES6語法中的類(或ES5的構造函數),通過new 類名()來創建,也可以用對象字面量語法創建。
- 其次要保證對象唯一。保證唯一可以採用只調用一次類的構造函數,再次調用直接返回已創建好的對象。當然,對象字面量語法創建的對象本身就是唯一的。
方式一: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()方法,即可以用單例模式去實現。