JavaScript設計模式經典之單例模式

alloyteam發表於2014-09-14

《Practical Common Lisp》的作者 Peter Seibel 曾說,如果你需要一種模式,那一定是哪裡出了問題。他所說的問題是指因為語言的天生缺陷,不得不去尋求和總結一種通用的解決方案。

不管是弱型別或強型別,靜態或動態語言,命令式或說明式語言、每種語言都有天生的優缺點。一個牙買加運動員, 在短跑甚至拳擊方面有一些優勢,在練瑜伽上就欠缺一些。

術士和暗影牧師很容易成為一個出色的輔助,而一個揹著梅肯滿地圖飛的敵法就會略顯尷尬。 換到程式中, 靜態語言裡可能需要花很多功夫來實現裝飾者,而js由於能隨時往物件上面扔方法,以至於裝飾者模式在js裡成了雞肋。

講 Javascript 設計模式的書還比較少,《Pro javaScript Design Patterns》是比較經典的一本,但是它裡面的例子舉得比較囉嗦,所以結合我在工作中寫過的程式碼,把我的理解總結一下。如果我的理解出現了偏差,請不吝指正。

一 單例模式

單例模式的定義是產生一個類的唯一例項,但js本身是一種“無類”語言。很多講js設計模式的文章把{}當成一個單例來使用也勉強說得通。因為js生成物件的方式有很多種,我們來看下另一種更有意義的單例。

有這樣一個常見的需求,點選某個按鈕的時候需要在頁面彈出一個遮罩層。比如web.qq.com點選登入的時候.

這個生成灰色背景遮罩層的程式碼是很好寫的.

var createMask = function(){

return document,body.appendChild(  document.createElement(div)  );

}

$( ‘button’ ).click( function(){

Var mask  = createMask();

mask.show();

})

問題是, 這個遮罩層是全域性唯一的, 那麼每次呼叫createMask都會建立一個新的div, 雖然可以在隱藏遮罩層的把它remove掉. 但顯然這樣做不合理.

再看下第二種方案, 在頁面的一開始就建立好這個div. 然後用一個變數引用它.

var mask = document.body.appendChild( document.createElement( ”div’ ) );

$( ”button’ ).click( function(){

mask.show();

} )

這樣確實在頁面只會建立一個遮罩層div, 但是另外一個問題隨之而來, 也許我們永遠都不需要這個遮罩層, 那又浪費掉一個div, 對dom節點的任何操作都應該非常吝嗇.

如果可以藉助一個變數. 來判斷是否已經建立過div呢?

var mask;

var createMask = function(){

if ( mask ) return mask;

else{

mask = document,body.appendChild(  document.createElement(div)  );

return mask;

}

}

看起來不錯, 到這裡的確完成了一個產生單列物件的函式. 我們再仔細看這段程式碼有什麼不妥.

首先這個函式是存在一定副作用的, 函式體內改變了外界變數mask的引用, 在多人協作的專案中, createMask是個不安全的函式. 另一方面, mask這個全域性變數並不是非需不可. 再來改進一下.

var createMask = function(){
var mask;
return function(){
return mask || ( mask = document.body.appendChild( document.createElement(‘div’) ) )
}
}()

用了個簡單的閉包把變數mask包起來, 至少對於createMask函式來講, 它是封閉的.

可能看到這裡, 會覺得單例模式也太簡單了. 的確一些設計模式都是非常簡單的, 即使從沒關注過設計模式的概念, 在平時的程式碼中也不知不覺用到了一些設計模式. 就像多年前我明白老漢推車是什麼回事的時候也想過尼瑪原來這就是老漢推車.

GOF裡的23種設計模式, 也是在軟體開發中早就存在並反覆使用的模式. 如果程式設計師沒有明確意識到他使用過某些模式, 那麼下次他也許會錯過更合適的設計 (這段話來自《松本行弘的程式世界》).

再回來正題, 前面那個單例還是有缺點. 它只能用於建立遮罩層. 假如我又需要寫一個函式, 用來建立一個唯一的xhr物件呢? 能不能找到一個通用的singleton包裝器.

js中函式是第一型, 意味著函式也可以當引數傳遞. 看看最終的程式碼.

var singleton = function( fn ){
var result;
return function(){
return result || ( result = fn .apply( this, arguments ) );
}
}

var createMask = singleton( function(){

return document.body.appendChild( document.createElement(‘div’) );

})

用一個變數來儲存第一次的返回值, 如果它已經被賦值過, 那麼在以後的呼叫中優先返回該變數. 而真正建立遮罩層的程式碼是通過回撥函式的方式傳人到singleton包裝器中的. 這種方式其實叫橋接模式. 關於橋接模式, 放在後面一點點來說.

然而singleton函式也不是完美的, 它始終還是需要一個變數result來寄存div的引用. 遺憾的是js的函式式特性還不足以完全的消除宣告和語句.

相關文章