前言
本系列文章主要根據《JavaScript設計模式與開發實踐》整理而來,其中會加入了一些自己的思考。希望對大家有所幫助。
文章系列
概念
單例模式的定義是:保證一個類僅有一個例項,並提供一個訪問它的全域性訪問點。
UML類圖
場景
單例模式是一種常用的模式,有一些物件我們往往只需要一個,比如執行緒池、全域性快取、瀏 覽器中的 window 物件等。
在 JavaScript 開發中,單例模式的用途同樣非常廣泛。試想一下,當我們單擊登入按鈕的時候,頁面中會出現一個登入浮窗,而這個登入浮窗是唯一的,無論單擊多少 次登入按鈕,這個浮窗都只會被建立一次,那麼這個登入浮窗就適合用單例模式來建立。
優缺點
優點:建立物件和管理單例的職責被分佈在兩個不同的方法中
實現
1. 我們的第一個單例
var instance = null
var getInstance = function(arg) {
if (!instance) {
instance = arg
}
return instance
}
var a = getInstance('a')
var b = getInstance('b')
console.log(a===b)
複製程式碼
這種定義一個全域性變數的方式非常不優雅,也不好複用程式碼
2. 利用閉包實現單例
var Singleton = function( name ){
this.name = name;
};
Singleton.getInstance = (function(){
var instance = null;
return function( name ){
if ( !instance ){
instance = new Singleton( name );
}
return instance;
}
})();
var a = Singleton.getInstance('a')
var b = Singleton.getInstance('b')
console.log(a===b)
複製程式碼
有些同學可能對閉包不大理解,下面用函式實現一下
3. 利用函式實現單例
function Singleton(name) {
this.name = name
this.instance = null
}
Singleton.getInstance = function(name) {
if (!this.instance) {
this.instance = new Singleton(name)
}
return this.instance
}
var a = Singleton.getInstance('a')
var b = Singleton.getInstance('b')
console.log(a===b)
複製程式碼
2,3這兩種方式也有缺點,就是我們必須呼叫getInstance來建立物件,一般我們建立物件都是利用new操作符
4. 透明的單例模式
var Singleton = (function() {
var instance
Singleton = function(name) {
if (instance) return instance
this.name = name
return instance = this
}
return Singleton
})()
var a = new Singleton('a')
var b = new Singleton('b')
console.log(a===b)
複製程式碼
這中方法也有點缺點:不符合單一職責原則,這個物件其實負責了兩個功能:單例和建立物件
下面我們分離這兩個職責
5. 利用代理實現單例
var People = function(name) {
this.name = name
}
var Singleton = (function() {
var instance
Singleton = function(name) {
if (instance) return instance
return instance = new People(name)
}
return Singleton
})()
var a = new Singleton('a')
var b = new Singleton('b')
console.log(a===b)
複製程式碼
這中方法也有點缺點:程式碼不能複用。如果我們有另外一個物件也要利用單例模式,那我們不得不寫重複的程式碼
6. 提供通用的單例
var People = function(name) {
this.name = name
}
var Singleton = function(Obj) {
var instance
Singleton = function() {
if (instance) return instance
return instance = new Obj(arguments)
}
return Singleton
}
var peopleSingleton = Singleton(People)
var a = new peopleSingleton('a')
var b = new peopleSingleton('b')
console.log(a===b)
複製程式碼
到這裡已經比較完美了,等等這只是es5的寫法,下面我們用es6來實現一下
7. es6單例模式
class People {
constructor(name) {
if (typeof People.instance === 'object') {
return People.instance;
}
People.instance = this;
this.name = name
return this;
}
}
var a = new People('a')
var b = new People('b')
console.log(a===b)
複製程式碼
比較以上幾種實現
- 用全域性變數的第1種方法,應該摒棄
- 用閉包實現的第2種方式,instance 例項物件總是在我們呼叫 Singleton.getInstance 的時候才被建立,應該摒棄
- 其他方式都是惰性單例(在需要時才建立)
js的特殊性
我們都知道:JavaScript 其實是一門無類(class-free)語言,,生搬單例模式的概念並無意義。
單例模式的核心是確保只有一個例項,並提供全域性訪問。
我們可以用一下幾種方式來另類實現
1. 全域性變數
比如var a = {},這時全域性就只有一個a物件 但全域性變數存在很多問題,它很容易造成名稱空間汙染,我們用以下兩種方式解決
2.使用名稱空間
var namespace1 = {
a: function () {
alert(1);
},
b: function () {
alert(2);
}
};
複製程式碼
另外我們還可以動態建立名稱空間
var MyApp = {};
MyApp.namespace = function (name) {
var parts = name.split('.');
var current = MyApp;
for (var i in parts) {
if (!current[parts[i]]) {
current[parts[i]] = {};
}
current = current[parts[i]];
}
};
MyApp.namespace('event');
MyApp.namespace('dom.style');
console.dir(MyApp);
// 上述程式碼等價於:
var MyApp = {
event: {},
dom: {
style: {}
}
};
複製程式碼
3. 閉包
var user = (function () {
var __name = 'sven',
__age = 29;
return {
getUserInfo: function () {
return __name + '-' + __age;
}
}
})();
複製程式碼
例子
登入框
下面我們來實現一個點選登入按鈕彈出登入框的例子
粗糙的實現
<html>
<body>
<button id="loginBtn">登入</button>
</body>
<script>
var loginLayer = (function () {
var div = document.createElement('div');
div.innerHTML = '我是登入浮窗';
div.style.display = 'none';
document.body.appendChild(div);
return div;
})();
document.getElementById('loginBtn').onclick = function () {
loginLayer.style.display = 'block';
};
</script>
</html>
複製程式碼
上面這種方式如果使用者沒有點選登入按鈕,也會在一開始就建立登入框
改進
<html>
<body>
<button id="loginBtn">登入</button>
</body>
<script>
var createLoginLayer = function () {
var div = document.createElement('div');
div.innerHTML = '我是登入浮窗';
div.style.display = 'none';
document.body.appendChild(div);
return div;
};
document.getElementById('loginBtn').onclick = function () {
var loginLayer = createLoginLayer();
loginLayer.style.display = 'block';
};
</script>
</html>
複製程式碼
這種方式每次點選按鈕都會建立一個登入框
再改進
var createLoginLayer = (function () {
var div;
return function () {
if (!div) {
div = document.createElement('div');
div.innerHTML = '我是登入浮窗';
div.style.display = 'none';
document.body.appendChild(div);
}
return div;
}
})();
document.getElementById('loginBtn').onclick = function () {
var loginLayer = createLoginLayer();
loginLayer.style.display = 'block';
};
複製程式碼
這種方式不夠通用,不符合單一職責原則
再再改進
var getSingle = function (fn) {
var result;
return function () {
return result || (result = fn.apply(this, arguments));
}
};
var createLoginLayer = function () {
var div = document.createElement('div');
div.innerHTML = '我是登入浮窗';
div.style.display = 'none';
document.body.appendChild(div);
return div;
};
var createSingleLoginLayer = getSingle(createLoginLayer);
document.getElementById('loginBtn').onclick = function () {
var loginLayer = createSingleLoginLayer();
loginLayer.style.display = 'block';
};
//下面我們再試試建立唯一的iframe 用於動態載入第三方頁面:
var createSingleIframe = getSingle(function () {
var iframe = document.createElement('iframe');
document.body.appendChild(iframe);
return iframe;
});
document.getElementById('loginBtn').onclick = function () {
var loginLayer = createSingleIframe();
loginLayer.src = 'http://baidu.com';
};
複製程式碼
至此已經完美