JavaScript設(shè)計(jì)模式之單例模式
單例模式
單例模式是一種常用的模式,有一些對(duì)象我們往往只需要一個(gè),比如線程池、全局緩存、瀏覽器中的 window 對(duì)象等。在 JavaScript 開發(fā)中,單例模式的用途同樣非常廣泛。試想一下,當(dāng)我 們單擊登錄按鈕的時(shí)候,頁(yè)面中會(huì)出現(xiàn)一個(gè)登錄浮窗,而這個(gè)登錄浮窗是唯一的,無(wú)論單擊多少 次登錄按鈕,這個(gè)浮窗都只會(huì)被創(chuàng)建一次,那么這個(gè)登錄浮窗就適合用單例模式來創(chuàng)建。
實(shí)現(xiàn)單例模式
要實(shí)現(xiàn)一個(gè)標(biāo)準(zhǔn)的單例模式并不復(fù)雜,無(wú)非是用一個(gè)變量來標(biāo)志當(dāng)前是否已經(jīng)為某個(gè)類創(chuàng)建 過對(duì)象,如果是,則在下一次獲取該類的實(shí)例時(shí),直接返回之前創(chuàng)建的對(duì)象。代碼如下:
class Singleton {
constructor(name) {
this.name = name;
this.instance = null;
}
static getInstance(name) {
if (this.instance === null) {
this.instance = new Singleton(name);
}
return this.instance;
}
}我們通過 Singleton.getInstance 來獲取 Singleton 類的唯一對(duì)象,這種方式相對(duì)簡(jiǎn)單,但有 一個(gè)問題,就是增加了這個(gè)類的“不透明性”,Singleton 類的使用者必須知道這是一個(gè)單例類, 跟以往通過 new XXX 的方式來獲取對(duì)象不同,這里偏要使用 Singleton.getInstance 來獲取對(duì)象。 接下來順便進(jìn)行一些小測(cè)試,來證明這個(gè)單例類是可以信賴的:
const a = Singleton.getInstance( '夏安1' ); const b = Singleton.getInstance( '夏安2' ); console.log(a === b); // true
雖然現(xiàn)在已經(jīng)完成了一個(gè)單例模式的編寫,但這段單例模式代碼的意義并不大。從下一節(jié)開 始,我們將一步步編寫出更好的單例模式。
透明的單例模式
我們現(xiàn)在的目標(biāo)是實(shí)現(xiàn)一個(gè)“透明”的單例類,用戶從這個(gè)類中創(chuàng)建對(duì)象的時(shí)候,可以像使 用其他任何普通類一樣。在下面的例子中,我們將使用 CreateDiv 單例類,它的作用是負(fù)責(zé)在頁(yè) 面中創(chuàng)建唯一的 div 節(jié)點(diǎn),代碼如下:
class CreateDiv {
constructor(html) {
if (!CreateDiv.instance) {
const div = document.createElement('div');
div.innerHTML = html;
document.body.appendChild(div);
CreateDiv.instance = div;
}
return CreateDiv.instance;
}
static instance = null;
}然而,假設(shè)我們某天需要利用這個(gè)類,在頁(yè)面中創(chuàng)建千千萬(wàn)萬(wàn)的 div,即要讓這個(gè)類從單例類變成 一個(gè)普通的可產(chǎn)生多個(gè)實(shí)例的類,那我們必須得改寫 CreateDiv 構(gòu)造函數(shù),把控制創(chuàng)建唯一對(duì)象的那一個(gè)靜態(tài)屬性去掉,這種修改會(huì)給我們帶來不必要的煩惱。
用代理實(shí)現(xiàn)單例模式
現(xiàn)在我們通過引入代理類的方式,來解決上面提到的問題。 我們依然使用上一節(jié)節(jié)中的代碼,首先 CreateDiv 構(gòu)造函數(shù)中,把負(fù)責(zé)管理單例的代碼移除 出去,使它成為一個(gè)普通的創(chuàng)建 div 的類:
class CreateDiv {
constructor(html) {
const div = document.createElement('div');
div.innerHTML = html;
document.body.appendChild(div);
return div;
}
}
class ProxySingletonCreateDiv {
constructor(html) {
if (!CreateDiv.instance) {
CreateDiv.instance = new CreateDiv(html);
}
return CreateDiv.instance;
}
static instance = null;
}通過引入代理類的方式,我們同樣完成了一個(gè)單例模式的編寫,跟之前不同的是,現(xiàn)在我們 把負(fù)責(zé)管理單例的邏輯移到了代理類 proxySingletonCreateDiv 中。這樣一來,CreateDiv 就變成了 一個(gè)普通的類,它跟 proxySingletonCreateDiv 組合起來可以達(dá)到單例模式的效果。
本例是緩存代理的應(yīng)用之一,之后我們將繼續(xù)了解代理帶來的好處。
惰性單例
前面我們了解了單例模式的一些實(shí)現(xiàn)辦法,本節(jié)我們來了解惰性單例。
惰性單例指的是在需要的時(shí)候才創(chuàng)建對(duì)象實(shí)例。惰性單例是單例模式的重點(diǎn),這種技術(shù)在實(shí)際開發(fā)中非常有用,有用的程度可能超出了我們的想象,實(shí)際上在本文開頭就使用過這種技術(shù), instance 實(shí)例對(duì)象總是在我們調(diào)用 Singleton.getInstance 的時(shí)候才被創(chuàng)建,而不是在頁(yè)面加載好的時(shí)候就創(chuàng)建,代碼如下:
static getInstance(name) {
if (this.instance === null) {
this.instance = new Singleton(name);
}
return this.instance;
}不過這是基于“類”的單例模式,下面我們將以 WebQQ 的登錄浮窗為例,介紹與全局變量結(jié)合實(shí)現(xiàn)惰性的單例。
假設(shè)我們是 WebQQ 的開發(fā)人員,當(dāng)點(diǎn)擊左邊導(dǎo)航里 QQ 頭像時(shí),會(huì)彈出一個(gè)登錄浮窗,很明顯這個(gè)浮窗在頁(yè)面里總是唯一的,不可能出現(xiàn)同時(shí)存在 兩個(gè)登錄窗口的情況。
第一種解決方案是在頁(yè)面加載完成的時(shí)候便創(chuàng)建好這個(gè) div 浮窗,這個(gè)浮窗一開始肯定是隱藏狀態(tài)的,當(dāng)用戶點(diǎn)擊登錄按鈕的時(shí)候,它才開始顯示:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>惰性單例</title>
</head>
<body>
<button id='login-button'>登錄</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('login-button').onclick = function() {
loginLayer.style.display = 'block';
}
</script>
</html>這種方式有一個(gè)問題,也許我們進(jìn)入 WebQQ 只是玩玩游戲或者看看天氣等,根本不需要進(jìn)行登錄操作,因?yàn)榈卿浉〈翱偸且婚_始就被創(chuàng)建好,那么很有可能將白白浪費(fèi)一些 DOM 節(jié)點(diǎn)。 現(xiàn)在改寫一下代碼,使用戶點(diǎn)擊登錄按鈕的時(shí)候才開始創(chuàng)建該浮窗:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>惰性單例</title>
</head>
<body>
<button id='login-button'>登錄</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('login-button').onclick = function() {
var loginLayer = createLoginLayer();
loginLayer.style.display = 'block';
}
</script>
</html>雖然現(xiàn)在達(dá)到了惰性的目的,但失去了單例的效果。當(dāng)我們每次點(diǎn)擊登錄按鈕的時(shí)候,都會(huì) 創(chuàng)建一個(gè)新的登錄浮窗 div。雖然我們可以在點(diǎn)擊浮窗上的關(guān)閉按鈕時(shí)(此處未實(shí)現(xiàn))把這個(gè)浮 窗從頁(yè)面中刪除掉,但這樣頻繁地創(chuàng)建和刪除節(jié)點(diǎn)明顯是不合理的,也是不必要的。
也許讀者已經(jīng)想到了,我們可以用一個(gè)變量來判斷是否已經(jīng)創(chuàng)建過登錄浮窗,這也是本節(jié)第 一段代碼中的做法:
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('login-button').onclick = function() {
var loginLayer = createLoginLayer();
loginLayer.style.display = 'block';
}通用的惰性單例
上一節(jié)我們完成了一個(gè)可用的惰性單例,但是我們發(fā)現(xiàn)它還有如下一些問題。
這段代碼仍然是違反單一職責(zé)原則的,創(chuàng)建對(duì)象和管理單例的邏輯都放在 createLoginLayer 對(duì)象內(nèi)部。
如果我們下次需要?jiǎng)?chuàng)建頁(yè)面中唯一的 iframe,或者 script 標(biāo)簽,用來跨域請(qǐng)求數(shù)據(jù),就必須得如法炮制,把 createLoginLayer 函數(shù)幾乎照抄一遍:
var createIframe = (function(){
var iframe;
return function() {
if (!iframe) {
iframe = document.createElement('iframe');
iframe.style.display = 'none';
document.body.appendChild(iframe);
}
return iframe;
}
})();
我們需要把不變的部分隔離出來,先不考慮創(chuàng)建一個(gè) div 和創(chuàng)建一個(gè) iframe 有多少差異,管理單例的邏輯其實(shí)是完全可以抽象出來的,這個(gè)邏輯始終是一樣的:用一個(gè)變量來標(biāo)志是否創(chuàng)建過對(duì)象,如果是,則在下次直接返回這個(gè)已經(jīng)創(chuàng)建好的對(duì)象:
var obj;
if ( !obj ){
obj = xxx;
}
現(xiàn)在我們就把如何管理單例的邏輯從原來的代碼中抽離出來,這些邏輯被封裝在 getSingle 函數(shù)內(nèi)部,創(chuàng)建對(duì)象的方法 fn 被當(dāng)成參數(shù)動(dòng)態(tài)傳入 getSingle 函數(shù):
var getSingle = function(fn){
var result;
return function() {
return result || (result = fn.apply(this, arguments));
}
};
接下來將用于創(chuàng)建登錄浮窗的方法用參數(shù) fn 的形式傳入 getSingle,我們不僅可以傳入 createLoginLayer,還能傳入 createScript、createIframe、createXhr 等。之后再讓 getSingle 返回 一個(gè)新的函數(shù),并且用一個(gè)變量 result 來保存 fn 的計(jì)算結(jié)果。result 變量因?yàn)樯碓陂]包中,它永遠(yuǎn)不會(huì)被銷毀。在將來的請(qǐng)求中,如果 result 已經(jīng)被賦值,那么它將返回這個(gè)值。代碼如下:
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';
};
在這個(gè)例子中,我們把創(chuàng)建實(shí)例對(duì)象的職責(zé)和管理單例的職責(zé)分別放置在兩個(gè)方法里,這兩個(gè)方法可以獨(dú)立變化而互不影響,當(dāng)它們連接在一起的時(shí)候,就完成了創(chuàng)建唯一實(shí)例對(duì)象的功能,看起來是一件挺奇妙的事情。
小結(jié)
單例模式是我們學(xué)習(xí)的第一個(gè)模式,我們先學(xué)習(xí)了傳統(tǒng)的單例模式實(shí)現(xiàn),也了解到因?yàn)檎Z(yǔ)言的差異性,有更適合的方法在 JavaScript 中創(chuàng)建單例。本文還提到了代理模式和單一職責(zé)原則, 后面的章節(jié)會(huì)對(duì)它們進(jìn)行更詳細(xì)的講解。
在 getSinge 函數(shù)中,實(shí)際上也提到了閉包和高階函數(shù)的概念。單例模式是一種簡(jiǎn)單但非常實(shí) 用的模式,特別是惰性單例技術(shù),在合適的時(shí)候才創(chuàng)建對(duì)象,并且只創(chuàng)建唯一的一個(gè)。更奇妙的 是,創(chuàng)建對(duì)象和管理單例的職責(zé)被分布在兩個(gè)不同的方法中,這兩個(gè)方法組合起來才具有單例模式的威力。
到此這篇關(guān)于JavaScript單例模式的文章就介紹到這了,更多相關(guān)JS單例模式內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
JS動(dòng)態(tài)加載當(dāng)前時(shí)間的方法
這篇文章主要介紹了JS動(dòng)態(tài)加載當(dāng)前時(shí)間的方法,涉及html的onload方法及javascript操作時(shí)間的技巧,需要的朋友可以參考下2015-02-02
JS實(shí)現(xiàn)網(wǎng)頁(yè)自動(dòng)刷新腳本的方法
要自動(dòng)刷新網(wǎng)頁(yè),你可以使用JavaScript腳本來實(shí)現(xiàn),下面這篇文章主要給大家介紹了關(guān)于JS實(shí)現(xiàn)網(wǎng)頁(yè)自動(dòng)刷新腳本的相關(guān)資料,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-11-11
JS+CSS實(shí)現(xiàn)滑動(dòng)切換tab菜單效果
這篇文章主要介紹了JS+CSS實(shí)現(xiàn)滑動(dòng)切換tab菜單效果,涉及javascript鼠標(biāo)事件及頁(yè)面元素樣式的動(dòng)態(tài)切換效果實(shí)現(xiàn)技巧,需要的朋友可以參考下2015-08-08
微信小程序?qū)崿F(xiàn)點(diǎn)擊按鈕修改字體顏色功能【附demo源碼下載】
這篇文章主要介紹了微信小程序?qū)崿F(xiàn)點(diǎn)擊按鈕修改字體顏色功能,涉及微信小程序wx:for循環(huán)讀取data數(shù)值及事件綁定修改元素屬性相關(guān)操作技巧,需要的朋友可以參考下2017-12-12
js中style.display=""無(wú)效的解決方法
這篇文章主要介紹了js中style.display=""無(wú)效的解決方法,是js程序設(shè)計(jì)中非常常見的問題,需要的朋友可以參考下2014-10-10
JS實(shí)現(xiàn)中國(guó)公民身份證號(hào)碼有效性驗(yàn)證
這篇文章主要介紹了JS實(shí)現(xiàn)中國(guó)公民身份證號(hào)碼有效性驗(yàn)證,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2017-02-02
整理Javascript基礎(chǔ)語(yǔ)法學(xué)習(xí)筆記
整理Javascript基礎(chǔ)語(yǔ)法學(xué)習(xí)筆記,之前一系列的文章是跟我學(xué)習(xí)Javascript,本文就是進(jìn)一步學(xué)習(xí)javascript基礎(chǔ)語(yǔ)法,希望大家繼續(xù)關(guān)注2015-11-11

