JavaScript設計模式之單例模式
單例模式
單例模式是一種常用的模式,有一些對象我們往往只需要一個,比如線程池、全局緩存、瀏覽器中的 window 對象等。在 JavaScript 開發(fā)中,單例模式的用途同樣非常廣泛。試想一下,當我 們單擊登錄按鈕的時候,頁面中會出現(xiàn)一個登錄浮窗,而這個登錄浮窗是唯一的,無論單擊多少 次登錄按鈕,這個浮窗都只會被創(chuàng)建一次,那么這個登錄浮窗就適合用單例模式來創(chuàng)建。
實現(xiàn)單例模式
要實現(xiàn)一個標準的單例模式并不復雜,無非是用一個變量來標志當前是否已經(jīng)為某個類創(chuàng)建 過對象,如果是,則在下一次獲取該類的實例時,直接返回之前創(chuàng)建的對象。代碼如下:
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
類的唯一對象,這種方式相對簡單,但有 一個問題,就是增加了這個類的“不透明性”,Singleton
類的使用者必須知道這是一個單例類, 跟以往通過 new XXX
的方式來獲取對象不同,這里偏要使用 Singleton.getInstance
來獲取對象。 接下來順便進行一些小測試,來證明這個單例類是可以信賴的:
const a = Singleton.getInstance( '夏安1' ); const b = Singleton.getInstance( '夏安2' ); console.log(a === b); // true
雖然現(xiàn)在已經(jīng)完成了一個單例模式的編寫,但這段單例模式代碼的意義并不大。從下一節(jié)開 始,我們將一步步編寫出更好的單例模式。
透明的單例模式
我們現(xiàn)在的目標是實現(xiàn)一個“透明”的單例類,用戶從這個類中創(chuàng)建對象的時候,可以像使 用其他任何普通類一樣。在下面的例子中,我們將使用 CreateDiv
單例類,它的作用是負責在頁 面中創(chuàng)建唯一的 div
節(jié)點,代碼如下:
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; }
然而,假設我們某天需要利用這個類,在頁面中創(chuàng)建千千萬萬的 div
,即要讓這個類從單例類變成 一個普通的可產(chǎn)生多個實例的類,那我們必須得改寫 CreateDiv
構(gòu)造函數(shù),把控制創(chuàng)建唯一對象的那一個靜態(tài)屬性去掉,這種修改會給我們帶來不必要的煩惱。
用代理實現(xiàn)單例模式
現(xiàn)在我們通過引入代理類的方式,來解決上面提到的問題。 我們依然使用上一節(jié)節(jié)中的代碼,首先 CreateDiv
構(gòu)造函數(shù)中,把負責管理單例的代碼移除 出去,使它成為一個普通的創(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; }
通過引入代理類的方式,我們同樣完成了一個單例模式的編寫,跟之前不同的是,現(xiàn)在我們 把負責管理單例的邏輯移到了代理類 proxySingletonCreateDiv
中。這樣一來,CreateDiv
就變成了 一個普通的類,它跟 proxySingletonCreateDiv
組合起來可以達到單例模式的效果。
本例是緩存代理的應用之一,之后我們將繼續(xù)了解代理帶來的好處。
惰性單例
前面我們了解了單例模式的一些實現(xiàn)辦法,本節(jié)我們來了解惰性單例。
惰性單例指的是在需要的時候才創(chuàng)建對象實例。惰性單例是單例模式的重點,這種技術(shù)在實際開發(fā)中非常有用,有用的程度可能超出了我們的想象,實際上在本文開頭就使用過這種技術(shù), instance
實例對象總是在我們調(diào)用 Singleton.getInstance
的時候才被創(chuàng)建,而不是在頁面加載好的時候就創(chuàng)建,代碼如下:
static getInstance(name) { if (this.instance === null) { this.instance = new Singleton(name); } return this.instance; }
不過這是基于“類”的單例模式,下面我們將以 WebQQ 的登錄浮窗為例,介紹與全局變量結(jié)合實現(xiàn)惰性的單例。
假設我們是 WebQQ 的開發(fā)人員,當點擊左邊導航里 QQ 頭像時,會彈出一個登錄浮窗,很明顯這個浮窗在頁面里總是唯一的,不可能出現(xiàn)同時存在 兩個登錄窗口的情況。
第一種解決方案是在頁面加載完成的時候便創(chuàng)建好這個 div 浮窗,這個浮窗一開始肯定是隱藏狀態(tài)的,當用戶點擊登錄按鈕的時候,它才開始顯示:
<!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>
這種方式有一個問題,也許我們進入 WebQQ 只是玩玩游戲或者看看天氣等,根本不需要進行登錄操作,因為登錄浮窗總是一開始就被創(chuàng)建好,那么很有可能將白白浪費一些 DOM 節(jié)點。 現(xiàn)在改寫一下代碼,使用戶點擊登錄按鈕的時候才開始創(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)在達到了惰性的目的,但失去了單例的效果。當我們每次點擊登錄按鈕的時候,都會 創(chuàng)建一個新的登錄浮窗 div
。雖然我們可以在點擊浮窗上的關(guān)閉按鈕時(此處未實現(xiàn))把這個浮 窗從頁面中刪除掉,但這樣頻繁地創(chuàng)建和刪除節(jié)點明顯是不合理的,也是不必要的。
也許讀者已經(jīng)想到了,我們可以用一個變量來判斷是否已經(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é)我們完成了一個可用的惰性單例,但是我們發(fā)現(xiàn)它還有如下一些問題。
這段代碼仍然是違反單一職責原則的,創(chuàng)建對象和管理單例的邏輯都放在 createLoginLayer
對象內(nèi)部。
如果我們下次需要創(chuàng)建頁面中唯一的 iframe
,或者 script
標簽,用來跨域請求數(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)建一個 div
和創(chuàng)建一個 iframe
有多少差異,管理單例的邏輯其實是完全可以抽象出來的,這個邏輯始終是一樣的:用一個變量來標志是否創(chuàng)建過對象,如果是,則在下次直接返回這個已經(jīng)創(chuàng)建好的對象:
var obj; if ( !obj ){ obj = xxx; }
現(xiàn)在我們就把如何管理單例的邏輯從原來的代碼中抽離出來,這些邏輯被封裝在 getSingle
函數(shù)內(nèi)部,創(chuàng)建對象的方法 fn
被當成參數(shù)動態(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
返回 一個新的函數(shù),并且用一個變量 result
來保存 fn
的計算結(jié)果。result
變量因為身在閉包中,它永遠不會被銷毀。在將來的請求中,如果 result
已經(jīng)被賦值,那么它將返回這個值。代碼如下:
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'; };
在這個例子中,我們把創(chuàng)建實例對象的職責和管理單例的職責分別放置在兩個方法里,這兩個方法可以獨立變化而互不影響,當它們連接在一起的時候,就完成了創(chuàng)建唯一實例對象的功能,看起來是一件挺奇妙的事情。
小結(jié)
單例模式是我們學習的第一個模式,我們先學習了傳統(tǒng)的單例模式實現(xiàn),也了解到因為語言的差異性,有更適合的方法在 JavaScript
中創(chuàng)建單例。本文還提到了代理模式和單一職責原則, 后面的章節(jié)會對它們進行更詳細的講解。
在 getSinge
函數(shù)中,實際上也提到了閉包和高階函數(shù)的概念。單例模式是一種簡單但非常實 用的模式,特別是惰性單例技術(shù),在合適的時候才創(chuàng)建對象,并且只創(chuàng)建唯一的一個。更奇妙的 是,創(chuàng)建對象和管理單例的職責被分布在兩個不同的方法中,這兩個方法組合起來才具有單例模式的威力。
到此這篇關(guān)于JavaScript單例模式的文章就介紹到這了,更多相關(guān)JS單例模式內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
關(guān)于BootStrap modal 在IOS9中不能彈出的解決方法(IOS 9 bootstrap modal ios
本文給大家介紹BootStrap modal 在IOS9中不能彈出的問題以及bootstrap datepicker 在bootstrap modal中不顯示問題的解決方案,非常不錯,需要的朋友參考下2016-12-12JavaScript實現(xiàn)兼容IE6的收起折疊與展開效果實例
這篇文章主要介紹了JavaScript實現(xiàn)兼容IE6的收起折疊與展開效果,結(jié)合具體實例形式分析了javascript事件響應及針對頁面元素屬性的動態(tài)操作相關(guān)實現(xiàn)技巧,需要的朋友可以參考下2017-09-09