單例模式:限制類實例化次數只能一次,一個類只有一個實例,並提供一個訪問它的全局訪問點。
單例模式是創建型設計模式的一種。針對全局僅需一個對象的場景,如線程池、全局緩存、window 對象等。
模式特點
- 類只有一個實例
- 全局可訪問該實例
- 自行實例化(主動實例化)
- 可推遲初始化,即延遲執行(與靜態類/對象的區別)
JavaScript 是一門非正規面向對象的語言,並沒有類的定義。而單例模式要求一個 “唯一” 和 “全局訪問” 的對象,在 JavaScript 中類似全局對象,剛好滿足單例模式的兩個特點:“唯一” 和 “可全局訪問”。雖然它不是正規的單例模式,但不可否認確實具備類單例模式的特點。
// 全局對象
var globaObj = {};
使用全局變量會有以下問題:
- 命名空間污染(變量名衝突)
- 維護時不方便管控(容易不小心覆蓋)
全局變量問題折中的應對方案:
- 使用命名空間
- 閉包封裝私有變量(利用函數作用域)
- ES6的 const/symbol
雖然全局變量可以實現單例,但因其自身的問題,不建議在實際項目中將其作爲單例模式的應用,特別是中大型項目的應用中,全局變量的維護該是考慮的成本。
模式實現
實現方式:使用一個變量存儲類實例對象(值初始爲
null/undefined
)。進行類實例化時,判斷類實例對象是否存在,存在則返回該實例,不存在則創建類實例後返回。多次調用類生成實例方法,返回同一個實例對象。
“簡單版” 單例模式:
let Singleton = function(name) {
this.name = name;
this.instance = null;
}
Singleton.getInstance = function(name) {
if (this.instance) {
return this.instance;
}
return this.instance = new Singleton(name);
}
var Winner = Singleton.getInstance('Winner');
var Looser = Singleton.getInstance('Looser');
console.log(Winner === Looser); // true
console.log(Winner.getName()); // 'Winner'
console.log(Looser.getName()); // 'Winner'
代碼中定義了一個 Singleton
函數,函數在 JavaScript 中是“一等公民“,可以爲其定義屬性方法。因此我們可以在函數 Singleton
中定義一個 getInstance()
方法來管控單例,並創建返回類實例對象,而不是通過傳統的 new
操作符來創建類實例對象。
this.instance
存儲創建的實例對象,每次接收到創建實例對象時,判斷 this.instance
是否有實例對象,有則返回,沒有則創建並更新 this.instance
值,因此無論調用多少次 getInstance()
,最終都只會返回同一個 Singleton
類實例對象。
存在問題:
- 不夠“透明”,無法使用
new
來進行類實例化,需約束該類實例化的調用方式:Singleton.getInstance(...)
; - 管理單例的操作,與對象創建的操作,功能代碼耦合在一起,不符合 “單一職責原則”
“透明版” 單例模式:
實現 “透明版” 單例模式,意圖解決:統一使用 new
操作符來獲取單例對象, 而不是 Singleton.getInstance(...)
。
let CreateSingleton = (function(){
let instance;
return function(name) {
if (instance) {
return instance;
}
this.name = name;
return instance = this;
}
})();
CreateSingleton.prototype.getName = function() {
console.log(this.name);
}
var Winner = new CreateSingleton('Winner');
var Looser = new CreateSingleton('Looser');
console.log(Winner === Looser); // true
console.log(Winner.getName()); // 'Winner'
console.log(Looser.getName()); // 'Winner'
“透明版”單例模式解決了不夠“透明”的問題,我們又可以使用 new
操作符來創建實例對象。
“代理版“ 單例模式:
通過“代理”的形式,意圖解決:將管理單例操作,與對象創建操作進行拆分,實現更小的粒度劃分,符合“單一職責原則”
let ProxyCreateSingleton = (function(){
let instance;
return function(name) {
// 代理函數僅作管控單例
if (instance) {
return instance;
}
return instance = new Singleton(name);
}
})();
// 獨立的Singleton類,處理對象實例
let Singleton = function(name) {
this.name = name;
}
Singleton.prototype.getName = function() {
console.log(this.name);
}
var Winner = new PeozyCreateSingleton('Winner');
var Looser = new PeozyCreateSingleton('Looser');
console.log(Winner === Looser); // true
console.log(Winner.getName()); // 'Winner'
console.log(Looser.getName()); // 'Winner'
惰性單例模式
惰性單例,意圖解決:需要時才創建類實例對象。對於懶加載的性能優化,想必前端開發者並不陌生。惰性單例也是解決 “按需加載” 的問題。
需求:頁面彈窗提示,多次調用,都只有一個彈窗對象,只是展示信息內容不同。
開發這樣一個全局彈窗對象,我們可以應用單例模式。爲了提升它的性能,我們可以讓它在我們需要調用時再去生成實例,創建 DOM 節點。
let getSingleton = function(fn) {
var result;
return function() {
return result || (result = fn.apply(this, arguments)); // 確定this上下文並傳遞參數
}
}
let createAlertMessage = function(html) {
var div = document.createElement('div');
div.innerHTML = html;
div.style.display = 'none';
document.body.appendChild(div);
return div;
}
var createSingleAlertMessage = getSingleton(createAlertMessage);
document.body.addEventListener('click', function(){
// 多次點擊只會產生一個彈窗
let alertMessage = createSingleAlertMessage('您的知識需要付費充值!');
alertMessage.style.display = 'block';
})
代碼中演示是一個通用的 “惰性單例” 的創建方式,如果還需要 createLoginLayer
登錄框, createFrame
Frame框, 都可以調用 getSingleton(...)
生成對應實例對象的方法。
適用場景
“單例模式的特點,意圖解決:維護一個全局實例對象。”
- 引用第三方庫(多次引用只會使用一個庫引用,如 jQuery)
- 彈窗(登錄框,信息提升框)
- 購物車 (一個用戶只有一個購物車)
- 全局態管理 store (Vuex / Redux)
項目中引入第三方庫時,重複多次加載庫文件時,全局只會實例化一個庫對象,如 jQuery
,lodash
,moment
..., 其實它們的實現理念也是單例模式應用的一種:
// 引入代碼庫 libs(庫別名)
if (window.libs != null) {
return window.libs; // 直接返回
} else {
window.libs = '...'; // 初始化
}
優缺點
- 優點:適用於單一對象,只生成一個對象實例,避免頻繁創建和銷燬實例,減少內存佔用。
- 缺點:不適用動態擴展對象,或需創建多個相似對象的場景。
TIPS: 多線程編程語言中,單例模式會涉及同步鎖的問題。而 JavaScript 是單線程的編程語言,暫可忽略該問題。
參考文章
本文首發Github,期待Star!
https://github.com/ZengLingYong/blog
作者:以樂之名
本文原創,有不當的地方歡迎指出。轉載請指明出處。