通用的惰性單例一
假設(shè)我們是WebQQ 的開(kāi)發(fā)人員(網(wǎng)址是web.qq.com),當(dāng)點(diǎn)擊左邊導(dǎo)航里QQ 頭像時(shí),會(huì)彈出一個(gè)登錄浮窗(如圖4-1 所示),很明顯這個(gè)浮窗在頁(yè)面里總是唯一的,不可能出現(xiàn)同時(shí)存在兩個(gè)登錄窗口的情況。

第一種解決方案是在頁(yè)面加載完成的時(shí)候便創(chuàng)建好這個(gè)div 浮窗,這個(gè)浮窗一開(kāi)始肯定是隱藏狀態(tài)的,當(dāng)用戶點(diǎn)擊登錄按鈕的時(shí)候,它才開(kāi)始顯示:
//html
<button id="loginBtn">登錄</button>
//js
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';
};
這種方式有一個(gè)問(wèn)題,也許我們進(jìn)入WebQQ 只是玩玩游戲或者看看天氣,根本不需要進(jìn)行登錄操作,因?yàn)榈卿浉〈翱偸且婚_(kāi)始就被創(chuàng)建好,那么很有可能將白白浪費(fèi)一些DOM節(jié)點(diǎn)。現(xiàn)在改寫一下代碼,使用戶點(diǎn)擊登錄按鈕的時(shí)候才開(kāi)始創(chuàng)建該浮窗:
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';
};
雖然現(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è)變量來(lái)判斷是否已經(jīng)創(chuàng)建過(guò)登錄浮窗,這也是本節(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('loginBtn').onclick = function () {
var loginLayer = createLoginLayer();
loginLayer.style.display = 'block';
};
但是我們發(fā)現(xiàn)它還有如下一些問(wèn)題。
- 這段代碼仍然是違反單一職責(zé)原則的,創(chuàng)建對(duì)象和管理單例的邏輯都放在createLoginLayer
對(duì)象內(nèi)部。 - 如果我們下次需要?jiǎng)?chuàng)建頁(yè)面中唯一的iframe,或者script 標(biāo)簽,用來(lái)跨域請(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;
}
})();
我們需要把不變的部分隔離出來(lái),先不考慮創(chuàng)建一個(gè)div 和創(chuàng)建一個(gè)iframe 有多少差異,管理單例的邏輯其實(shí)是完全可以抽象出來(lái)的,這個(gè)邏輯始終是一樣的:用一個(gè)變量來(lái)標(biāo)志是否創(chuàng)建過(guò)對(duì)象,如果是,則在下次直接返回這個(gè)已經(jīng)創(chuàng)建好的對(duì)象:這些邏輯被封裝在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 ) );
}
};
接下來(lái)將用于創(chuàng)建登錄浮窗的方法用參數(shù)fn 的形式傳入getSingle,我們不僅可以傳入createLoginLayer,還能傳入createScript、createIframe、createXhr 等。之后再讓getSingle 返回一個(gè)新的函數(shù),并且用一個(gè)變量result 來(lái)保存fn 的計(jì)算結(jié)果。result 變量因?yàn)樯碓陂]包中,它永遠(yuǎn)不會(huì)被銷毀。在將來(lái)的請(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ì)象的功能,看起來(lái)是一件挺奇妙的事情。
通用的惰性單例二
這種單例模式的用途遠(yuǎn)不止創(chuàng)建對(duì)象,比如我們通常渲染完頁(yè)面中的一個(gè)列表之后,接下來(lái)要給這個(gè)列表綁定click 事件,如果是通過(guò)ajax 動(dòng)態(tài)往列表里追加數(shù)據(jù),在使用事件代理的前提下,click 事件實(shí)際上只需要在第一次渲染列表的時(shí)候被綁定一次,但是我們不想去判斷當(dāng)前是否是第一次渲染列表,如果借助于jQuery,我們通常選擇給節(jié)點(diǎn)綁定one 事件:
var bindEvent = function () {
$('div').one('click', function () {
alert('click');
});
};
var render = function () {
console.log('開(kāi)始渲染列表');
bindEvent();
};
render();
render();
render();
如果利用getSingle 函數(shù),也能達(dá)到一樣的效果。代碼如下:
var getSingle = function (fn) {
var result;
return function () {
return result ? result : result = fn.apply(this, arguments);
}
}
var bindEvent = getSingle(function () {
document.getElementById('loginBtn').addEventListener("click", function () {
alert('click');
});
return true;
});
var render = function () {
console.log('開(kāi)始渲染列表');
bindEvent();
};
render();
render();
render();