單例模式是一種常見的模式,如果希望系統中一個類只有一個執行個體,那麼單例模式是最好的解決方案。
一. 單例模式的定義
單例模式的定義:保證一個類僅有一個執行個體,並提供一個訪問它的全域訪問點。
二. 單例模式的實現原理
用一個變數來標誌當前是否已經為某個類建立過對象。如果是,則在下一次擷取該類的執行個體時,直接返回之前建立的對象。
三. 單例模式的優點
單例模式的優點有:
記憶體中只有一個對象,節省記憶體空間;
避免頻繁銷毀對象,提高效能;
避免共用資源多重佔用;
可以全域訪問。
四. 單例模式的實現方法
單例模式的核心是確保只有一個執行個體,並提供全域訪問。因此,單例模式的實現都是圍繞著如何確保執行個體的唯一性,因此需要用一個變數來標誌當前是否已經為某個類建立過執行個體對象。
實現方法主要有以下四種:
方法一: 可以使用全域變數來儲存該執行個體。但是全域變數容易被覆寫。
方法二: 可以在建構函式的靜態屬性中緩衝該執行個體。缺點是建構函式的靜態屬性是公開可訪問的屬性,在外部容易被覆寫。
方法三: 可以將該執行個體封裝在閉包中。這樣可以保證該執行個體的私人性並保證該執行個體不會被建構函式之外的代碼所修改。其代價是帶來額外的閉包開銷。
方法四:可以重寫建構函式。
這四種方法的具體實現如下:
4.1 使用全域變數儲存單例
var instance;
function Cat() {
if(typeof instance === "object") {
return instance;
}
// Cat建構函式方法 ... ...
instance = this;
}
/*============== 測試代碼 ===============*/
var cat1 = new Cat();
var cat2 = new Cat();
console.log(cat1 === cat2); // true
使用全域變數來儲存該執行個體對象是有風險的,不推薦使用這種方法。因為全域變數可以被任何人覆蓋,容易被覆寫,而使該執行個體對象丟失,從而導致意外事件。
應盡量減少全域變數的使用。即使需要,也應把它的汙染降到最低。例如採用命名空間、使用閉包封裝私人變數等方法,將全域變數帶來的命名汙染盡量降低。
4.2 在建構函式的靜態屬性中緩衝執行個體
function Cat() {
if(typeof Cat.instance === "object") {
return Cat.instance;
}
// Cat建構函式方法 ... ...
// 使用建構函式的靜態屬性來緩衝執行個體
Cat.instance = this;
}
/*============== 測試代碼 ===============*/
var cat1 = new Cat();
var cat2 = new Cat();
console.log(cat1 === cat2); // true
這是一個非常直接的解決方案,其唯一的缺點在於建構函式的靜態instance屬性是公開的。一旦其他代碼無意間修改了該屬性,則有可能導致意外發生。
4.3 使用閉包建立對象
var Cat = (function() {
var instance,
_this = this;
return function() {
if(typeof instance !== "object") {
// Cat建構函式方法 ... ...
instance = _this;
}
return instance;
}
})();
/*============== 測試代碼 ===============*/
var cat1 = new Cat();
var cat2 = new Cat();
console.log(cat1 === cat2); // true
4.4 重寫建構函式
function Cat() {
var instance = this;
// Cat建構函式方法 ... ...
// 重寫建構函式
Cat = function() {
return instance;
}
}
/*============== 測試代碼 ===============*/
var cat1 = new Cat();
var cat2 = new Cat();
console.log(cat1 === cat2); // true
這種模式的缺點,主要在於重寫建構函式中,建構函式會丟失所有在初始定義和重定義時刻之間添加到它裡面的屬性。
在上述代碼中,在重寫建構函式後,任何添加到Cat()的原型中的對象都不會存在指向由原始建構函式所建立的執行個體的指標。如下:
/*============== 測試代碼 ===============*/
// 向原型中添加屬性
Cat.prototype.color = "white";
var cat1 = new Cat();
// 建立初始化對象後,再次向該原型添加屬性
Cat.prototype.age = "1";
var cat2 = new Cat();
console.log(cat1 === cat2); // true
console.log(cat1.color); // white
console.log(cat2.color); // white
console.log(cat1.age); // undefined
console.log(cat2.age); // undefined
console.log(cat1.constructor === Cat); // false
console.log(cat2.constructor === Cat); // false
cat1.constructor不再與Cat()建構函式相同,是因為cat1.constructor指向了原始的建構函式,而不是重新定義的那個建構函式。所以在重寫建構函式後,任何添加到Cat()的原型中的對象都不會存在指向由原始建構函式所建立的執行個體的指標。
如果希望在重寫建構函式後,使原型中的對象指向原始建構函式,可以做以下調整:
function Cat() {
var instance = this;
// 重寫建構函式
Cat = function() {
return instance;
}
// 保留原型屬性
Cat.prototype = this;
// 建立執行個體
instance = new Cat();
// 重設建構函式指標
instance.contructor = Cat;
// Cat建構函式方法 ... ...
return instance;
}
/*============== 測試代碼 ===============*/
// 向原型中添加屬性
Cat.prototype.color = "white";
var cat1 = new Cat();
// 建立初始化對象後,再次向該原型添加屬性
Cat.prototype.age = "1";
var cat2 = new Cat();
console.log(cat1 === cat2); // true
console.log(cat1.color); // white
console.log(cat2.color); // white
console.log(cat1.age); // 1
console.log(cat2.age); // 1
五. 單例模式的最佳化與實際應用
5.1 使用閉包封裝建構函式和執行個體
將建構函式和執行個體封裝在幾時執行函數中,這樣在第一次調用建構函式時,它會建立一個對象,並且使得私人instance指向該對象。從第二次調用之後,該建構函式僅返回該私人變數。這樣不僅可以避免全域變數汙染,也可以實現單例。
var Cat;
(function() {
var instance;
Cat = function() {
if(instance) {
return instance;
}
// 使用建構函式的靜態屬性來緩衝執行個體
instance = this;
// Cat建構函式的功能實現 ... ...
// ... ...
};
})();
/*============== 測試代碼 ===============*/
var cat1 = new Cat();
var cat2 = new Cat();
console.log(cat1 === cat2); // true
5.2 用代理實現單例模式
可以引入代理類,在代理中實現一個類只能初始化一個執行個體。
function Cat() {
// Cat建構函式方法 ... ...
}
var ProxyCat = (function() {
var instance;
return function() {
if(typeof instance !== "object") {
instance = new Cat();
}
return instance;
}
})();
/*============== 測試代碼 ===============*/
var cat1 = new ProxyCat();
var cat2 = new ProxyCat();
console.log(cat1 === cat2); // true
引入代理類的方式,跟之前不同的是,把負責管理單類的邏輯移到了代理類中。這樣Cat就變成了一個普通的類。
5.3 通用的惰性單例
將管理單例的邏輯從原來的代碼中抽離出來,並將這些邏輯封裝在getSingle函數內部,建立對象的方法fn被當成參數動態傳入getSingle函數:
var getSingle = function(fn) {
var result;
return function() {
return result || (result = fn.apply(this, arguments));
}
}
備忘:該代碼摘抄自《JavaScript設計模式與開發實踐》第四章 P68。
這樣,Cat的單例模式代碼可以改寫為:
function Cat() {
// Cat建構函式方法 ... ...
}
var createCat = getSingle(Cat);
/*============== 測試代碼 ===============*/
var cat1 = createCat();
var cat2 = createCat();
console.log(cat1 === cat2); // true
在這個例子中,把建立執行個體對象的職責和管理單例的職責分別放在兩個方法中,這兩個方法可以獨立變化而互不影響,並實現了建立唯一執行個體對象的功能。推薦使用。