DIV CSS 佈局教程網

 DIV+CSS佈局教程網 >> 網頁腳本 >> JavaScript入門知識 >> 關於JavaScript >> 常用的Javascript設計模式小結
常用的Javascript設計模式小結
編輯:關於JavaScript     

《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的函數式特性還不足以完全的消除聲明和語句.

二 簡單工廠模式

簡單工廠模式是由一個方法來決定到底要創建哪個類的實例, 而這些實例經常都擁有相同的接口. 這種模式主要用在所實例化的類型在編譯期並不能確定, 而是在執行期決定的情況。 說的通俗點,就像公司茶水間的飲料機,要咖啡還是牛奶取決於你按哪個按鈕。

簡單工廠模式在創建ajax對象的時候也非常有用.

之前我寫了一個處理ajax異步嵌套的庫,地址在https://github.com/AlloyTeam/DanceRequest.

這個庫裡提供了幾種ajax請求的方式,包括xhr對象的get, post, 也包括跨域用的jsonp和iframe. 為了方便使用, 這幾種方式都抽象到了同一個接口裡面.

var request1 = Request('cgi.xx.com/xxx' , ''get' );
request1.start();
request1.done( fn );
var request2 = Request('cgi.xx.com/xxx' , ''jsonp' );
request2.start();
request2.done( fn );

Request實際上就是一個工廠方法, 至於到底是產生xhr的實例, 還是jsonp的實例. 是由後來的代碼決定的。

實際上在js裡面,所謂的構造函數也是一個簡單工廠。只是批了一件new的衣服. 我們扒掉這件衣服看看裡面。

通過這段代碼, 在firefox, chrome等浏覽器裡,可以完美模擬new.

function A( name ){
this.name = name;
}
function ObjectFactory(){
var obj = {},
Constructor = Array.prototype.shift.call( arguments );
obj.__proto__ = typeof Constructor .prototype === 'number' ? Object.prototype
: Constructor .prototype;
var ret = Constructor.apply( obj, arguments );
return typeof ret === 'object' ? ret : obj;
}
var a = ObjectFactory( A, 'svenzeng' );
alert ( a.name ); 
//svenzeng

這段代碼來自es5的new和構造器的相關說明, 可以看到,所謂的new, 本身只是一個對象的復制和改寫過程, 而具體會生成什麼是由調用ObjectFactory時傳進去的參數所決定的。

三 觀察者模式

觀察者模式( 又叫發布者-訂閱者模式 )應該是最常用的模式之一. 在很多語言裡都得到大量應用. 包括我們平時接觸的dom事件. 也是js和dom之間實現的一種觀察者模式.

div.onclick = function click (){
alert ( ''click' )
}

只要訂閱了div的click事件. 當點擊div的時候, function click就會被觸發.

那麼到底什麼是觀察者模式呢. 先看看生活中的觀察者模式。

好萊塢有句名言. “不要給我打電話, 我會給你打電話”. 這句話就解釋了一個觀察者模式的來龍去脈。 其中“我”是發布者, “你”是訂閱者。

再舉個例子,我來公司面試的時候,完事之後每個面試官都會對我說:“請留下你的聯系方式, 有消息我們會通知你”。 在這裡“我”是訂閱者, 面試官是發布者。所以我不用每天或者每小時都去詢問面試結果, 通訊的主動權掌握在了面試官手上。而我只需要提供一個聯系方式。

觀察者模式可以很好的實現2個模塊之間的解耦。 假如我正在一個團隊裡開發一個html5游戲. 當游戲開始的時候,需要加載一些圖片素材。加載好這些圖片之後開始才執行游戲邏輯. 假設這是一個需要多人合作的項目. 我完成了Gamer和Map模塊, 而我的同事A寫了一個圖片加載器loadImage.

loadImage的代碼如下

loadImage( imgAry, function(){
Map.init();
Gamer.init();
} )

當圖片加載好之後, 再渲染地圖, 執行游戲邏輯. 嗯, 這個程序運行良好. 突然有一天, 我想起應該給游戲加上聲音功能. 我應該讓圖片加載器添上一行代碼.

loadImage( imgAry, function(){
Map.init();
Gamer.init();
Sount.init();
} )

可是寫這個模塊的同事A去了外地旅游. 於是我打電話給他, 喂. 你的loadImage函數在哪, 我能不能改一下, 改了之後有沒有副作用. 如你所想, 各種不淡定的事發生了. 如果當初我們能這樣寫呢:

loadImage.listen( ''ready', function(){
Map.init();
})
loadImage.listen( ''ready', function(){
Gamer.init();
})
loadImage.listen( ''ready', function(){
Sount.init();
})

loadImage完成之後, 它根本不關心將來會發生什麼, 因為它的工作已經完成了. 接下來它只要發布一個信號.

loadImage.trigger( ”ready' );

那麼監聽了loadImage的'ready'事件的對象都會收到通知. 就像上個面試的例子. 面試官根本不關心面試者們收到面試結果後會去哪吃飯. 他只負責把面試者的簡歷搜集到一起. 當面試結果出來時照著簡歷上的電話挨個通知.

說了這麼多概念, 來一個具體的實現. 實現過程其實很簡單. 面試者把簡歷扔到一個盒子裡, 然後面試官在合適的時機拿著盒子裡的簡歷挨個打電話通知結果.

Events = function() {
var listen, log, obj, one, remove, trigger, __this;
obj = {};
__this = this;
listen = function( key, eventfn ) { 
//把簡歷扔盒子, key就是聯系方式.
var stack, _ref; 
//stack是盒子
stack = ( _ref = obj[key] ) != null ? _ref : obj[ key ] = [];
return stack.push( eventfn );
};
one = function( key, eventfn ) {
remove( key );
return listen( key, eventfn );
};
remove = function( key ) {
var _ref;
return ( _ref = obj[key] ) != null ? _ref.length = 0 : void 0;
};
trigger = function() { 
//面試官打電話通知面試者
var fn, stack, _i, _len, _ref, key;
key = Array.prototype.shift.call( arguments );
stack = ( _ref = obj[ key ] ) != null ? _ref : obj[ key ] = [];
for ( _i = 0, _len = stack.length; _i < _len; _i++ ) {
fn = stack[ _i ];
if ( fn.apply( __this, arguments ) === false) {
return false;
}
}
return {
listen: listen,
one: one,
remove: remove,
trigger: trigger
}
}

最後用觀察者模式來做一個成人電視台的小應用.

//訂閱者

var adultTv = Event();
adultTv .listen( ''play', function( data ){
alert ( "今天是誰的電影" + data.name );
});
//發布者
adultTv .trigger( ''play', { 'name': '麻生希' } )

四 適配器模式

去年年前當時正在開發dev.qplus.com, 有個存儲應用分類id的js文件, 分類id的結構最開始設計的比較笨重. 於是我決定重構它. 我把它定義成一個json樹的形式, 大概是這樣:

var category = {
music: {
id: 1,
children: [ , , , , ]
}
}

dev.qplus.com裡大概有4,5個頁面都調用這個category對象. 春節前我休了1個星期假. 過年來之後發現郵箱裡有封郵件, 設計數據庫的同學把category..js也重構了一份, 並且其他幾個項目裡都是用了這份category.js, 我拿過來一看就傻眼了, 和我之前定的數據結構完全不一樣.

當然這是一個溝通上的反面例子. 但接下來的重點是我已經在N個文件裡用到了之前我定的category.js. 而且惹上了一些復雜的相關邏輯. 怎麼改掉我之前的代碼呢. 全部重寫肯定是不願意. 所以現在適配器就派上用場了.

只需要把同事的category用一個函數轉成跟我之前定義的一樣.

my.category = adapterCategory ( afu.category );

適配器模式的作用很像一個轉接口. 本來iphone的充電器是不能直接插在電腦機箱上的, 而通過一個usb轉接口就可以了.

所以, 在程序裡適配器模式也經常用來適配2個接口, 比如你現在正在用一個自定義的js庫. 裡面有個根據id獲取節點的方法$id(). 有天你覺得jquery裡的$實現得更酷, 但你又不想讓你的工程師去學習新的庫和語法. 那一個適配器就能讓你完成這件事情.

$id = function( id ){
return jQuery( '#' + id )[0];
}

五 代理模式

代理模式的定義是把對一個對象的訪問, 交給另一個代理對象來操作.

舉一個例子, 我在追一個MM想給她送一束花,但是我因為我性格比較腼腆,所以我托付了MM的一個好朋友來送。

這個例子不是非常好, 至少我們沒看出代理模式有什麼大的用處,因為追MM更好的方式是送一台寶馬。

再舉個例子,假如我每天都得寫工作日報( 其實沒有這麼慘 ). 我的日報最後會讓總監審閱. 如果我們都直接把日報發給 總監 , 那可能 總監 就沒法工作了. 所以通常的做法是把日報發給我的組長 , 組長把所有組員一周的日報都匯總後再發給總監 .

實際的編程中, 這種因為性能問題使用代理模式的機會是非常多的。比如頻繁的訪問dom節點, 頻繁的請求遠程資源. 可以把操作先存到一個緩沖區, 然後自己選擇真正的觸發時機.

再來個詳細的例子,之前我寫了一個街頭霸王的游戲, 地址在http://alloyteam.github.com/StreetFighter/

 

游戲中隆需要接受鍵盤的事件, 來完成相應動作.

於是我寫了一個keyManage類. 其中在游戲主線程裡監聽keyManage的變化.

var keyMgr = keyManage();
keyMgr.listen( ''change', function( keyCode ){
console.log( keyCode );
});

圖片裡面隆正在放升龍拳, 升龍拳的操作是前下前+拳. 但是這個keyManage類只要發生鍵盤事件就會觸發之前監聽的change函數. 這意味著永遠只能取得前,後,前,拳這樣單獨的按鍵事件,而無法得到一個按鍵組合。

好吧,我決定改寫我的keyManage類, 讓它也支持傳遞按鍵組合. 但是如果我以後寫個html5版雙截龍,意味著我每次都得改寫keyManage. 我總是覺得, 這種函數應該可以抽象成一個更底層的方法, 讓任何游戲都可以用上它.

所以最後的keyManage只負責映射鍵盤事件. 而隆接受到的動作是通過一個代理對象處理之後的.

var keyMgr = keyManage();
keyMgr.listen( ''change', proxy( function( keyCode ){
console.log( keyCode ); 
//前下前+拳
)} );

至於proxy裡面怎麼實現,完全可以自由發揮。

還有個例子就是在調用ajax請求的時候,無論是各種開源庫,還是自己寫的Ajax類, 都會給xhr對象設置一個代理. 我們不可能頻繁的去操作xhr對象發請求, 而應該是這樣.

var request = Ajax.get( 'cgi.xx.com/xxx' );
request.send();
request.done(function(){
});

六 橋接模式

橋接模式的作用在於將實現部分和抽象部分分離開來, 以便兩者可以獨立的變化。在實現api的時候, 橋接模式特別有用。比如最開始的singleton的例子.

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是抽象部分, 而createMask是實現部分。 他們完全可以獨自變化互不影響。 如果需要再寫一個單例的createScript就一點也不費力.

var createScript = singleton( function(){
return document.body.appendChild( document.createElement('script') );
})

另外一個常見的例子就是forEach函數的實現, 用來迭代一個數組.

forEach = function( ary, fn ){
for ( var i = 0, l = ary.length; i < l; i++ ){
var c = ary[ i ];
if ( fn.call( c, i, c ) === false ){
return false;
}
}
}

可以看到, forEach函數並不關心fn裡面的具體實現. fn裡面的邏輯也不會被forEach函數的改寫影響.

forEach( [1,2,3], function( i, n ){
alert ( n*2 )
} )
forEach( [1,2,3], function( i, n ){
alert ( n*3 )
} )

七 外觀模式

外觀模式(門面模式),是一種相對簡單而又無處不在的模式。外觀模式提供一個高層接口,這個接口使得客戶端或子系統更加方便調用。
用一段再簡單不過的代碼來表示

var getName = function(){
return ''svenzeng"
}
var getSex = function(){
return 'man'
}

如果你需要分別調用getName和getSex函數. 那可以用一個更高層的接口getUserInfo來調用.

var getUserInfo = function(){
var info = a() + b();
return info;
}

也許你會問為什麼一開始不把getName和getSex的代碼寫到一起, 比如這樣

var getNameAndSex = function(){
return 'svenzeng" + "man";
}

答案是顯而易見的,飯堂的炒菜師傅不會因為你預定了一份燒鴨和一份白菜就把這兩樣菜炒在一個鍋裡。他更願意給你提供一個燒鴨飯套餐。同樣在程序設計中,我們需要保證函數或者對象盡可能的處在一個合理粒度,畢竟不是每個人喜歡吃燒鴨的同時又剛好喜歡吃白菜。
外觀模式還有一個好處是可以對用戶隱藏真正的實現細節,用戶只關心最高層的接口。比如在燒鴨飯套餐的故事中,你並不關心師傅是先做燒鴨還是先炒白菜,你也不關心那只鴨子是在哪裡成長的。

最後寫個我們都用過的外觀模式例子

var stopEvent = function( e ){ 
//同時阻止事件默認行為和冒泡
e.stopPropagation();
e.preventDefault();
}

八 訪問者模式

GOF官方定義: 訪問者模式是表示一個作用於某個對象結構中的各元素的操作。它使可以在不改變各元素的類的前提下定義作用於這些元素的新操作。我們在使用一些操作對不同的對象進行處理時,往往會根據不同的對象選擇不同的處理方法和過程。在實際的代碼過程中,我們可以發現,如果讓所有的操作分散到各個對象中,整個系統會變得難以維護和修改。且增加新的操作通常都要重新編譯所有的類。因此,為了解決這個問題,我們可以將每一個類中的相關操作提取出來,包裝成一個獨立的對象,這個對象我們就稱為訪問者(Visitor)。利用訪問者,對訪問的元素進行某些操作時,只需將此對象作為參數傳遞給當前訪問者,然後,訪問者會依據被訪問者的具體信息,進行相關的操作。

據統計,上面這段話只有5%的人會看到最後一句。那麼通俗點講,訪問者模式先把一些可復用的行為抽象到一個函數(對象)裡,這個函數我們就稱為訪問者(Visitor)。如果另外一些對象要調用這個函數,只需要把那些對象當作參數傳給這個函數,在js裡我們經常通過call或者apply的方式傳遞this對象給一個Visitor函數.
訪問者模式也被稱為GOF總結的23種設計模式中最難理解的一種。不過這有很大一部分原因是因為《設計模式》基於C++和Smalltalk寫成. 在強類型語言中需要通過多次重載來實現訪問者的接口匹配。

而在js這種基於鴨子類型的語言中,訪問者模式幾乎是原生的實現, 所以我們可以利用apply和call毫不費力的使用訪問者模式,這一小節更關心的是這種模式的思想以及在js引擎中的實現。

我們先來了解一下什麼是鴨子類型,說個故事:
很久以前有個皇帝喜歡聽鴨子呱呱叫,於是他召集大臣組建一個一千只鴨子的合唱團。大臣把全國的鴨子都抓來了,最後始終還差一只。有天終於來了一只自告奮勇的雞,這只雞說它也會呱呱叫,好吧在這個故事的設定裡,它確實會呱呱叫。 後來故事的發展很明顯,這只雞混到了鴨子的合唱團中。— 皇帝只是想聽呱呱叫,他才不在乎你是鴨子還是雞呢。

這個就是鴨子類型的概念,在js這種弱類型語言裡,很多方法裡都不做對象的類型檢測,而是只關心這些對象能做什麼。
Array構造器和String構造器的prototype上的方法就被特意設計成了訪問者。這些方法不對this的數據類型做任何校驗。這也就是為什麼arguments能冒充array調用push方法.

看下v8引擎裡面Array.prototype.push的代碼:

function ArrayPush() { var n = TO_UINT32( this.length );
var m = %_ArgumentsLength(); for (var i = 0; i < m; i++) { this[i+n] = %_Arguments(i); 
//屬性拷貝 } this.length = n + m; //修正length return this.length;}

可以看到,ArrayPush方法沒有對this的類型做任何顯示的限制,所以理論上任何對象都可以被傳入ArrayPush這個訪問者。

不過在代碼的執行期,還是會受到一些隱式限制,在上面的例子很容易看出要求:

1、 this對象上面可儲存屬性. //反例: 值類型的數據
2、 this的length屬性可寫. //反例: functon對象, function有一個只讀的length屬性, 表示形參個數.

如果不符合這2條規則的話,代碼在執行期會報錯. 也就是說, Array.prototype.push.call( 1, ‘first' )和Array.prototoype.push.call( function(){}, ‘first' )都達不到預期的效果.

利用訪問者,我們來做個有趣的事情. 給一個object對象增加push方法.

var Visitor = {}
Visitor .push = function(){
return Array.prototype.push.apply( this, arguments );
}
var obj = {};
obj.push = Visitor .push;
obj.push( '"first" );
alert ( obj[0] ) 
//"first"
alert ( obj.length ); 
//1

九 策略模式

策略模式的意義是定義一系列的算法,把它們一個個封裝起來,並且使它們可相互替換。
一個小例子就能讓我們一目了然。

回憶下jquery裡的animate方法.

$( div ).animate( {"left: 200px"}, 1000, 'linear' ); 
//勻速運動
$( div ).animate( {"left: 200px"}, 1000, 'cubic' ); 
//三次方的緩動

這2句代碼都是讓div在1000ms內往右移動200個像素. linear(勻速)和cubic(三次方緩動)就是一種策略模式的封裝.
再來一個例子. 上半年我寫的dev.qplus.com, 很多頁面都會有個即時驗證的表單. 表單的每個成員都會有一些不同的驗證規則.

 

比如姓名框裡面, 需要驗證非空,敏感詞,字符過長這幾種情況。 當然是可以寫3個if else來解決,不過這樣寫代碼的擴展性和維護性可想而知。如果表單裡面的元素多一點,需要校驗的情況多一點,加起來寫上百個if else也不是沒有可能。
所以更好的做法是把每種驗證規則都用策略模式單獨的封裝起來。需要哪種驗證的時候只需要提供這個策略的名字。就像這樣:

nameInput.addValidata({
notNull: true,
dirtyWords: true,
maxLength: 30
})
而notNull,maxLength等方法只需要統一的返回true或者false,來表示是否通過了驗證。
validataList = {
notNull: function( value ){
return value !== '';
},
maxLength: function( value, maxLen ){
return value.length() > maxLen;
}
}

可以看到,各種驗證規則很容易被修改和相互替換。如果某天產品經理建議字符過長的限制改成60個字符。那只需要0.5秒完成這次工作。

十 模版方法模式

模式方法是預先定義一組算法,先把算法的不變部分抽象到父類,再將另外一些可變的步驟延遲到子類去實現。聽起來有點像工廠模式( 非前面說過的簡單工廠模式 ).
最大的區別是,工廠模式的意圖是根據子類的實現最終獲得一種對象. 而模版方法模式著重於父類對子類的控制.

按GOF的描敘,模版方法導致一種反向的控制結構,這種結構有時被稱為“好萊塢法則”,即“別找我們,我們找你”。這指的是一個父類調用一個子類的操作,而不是相反。
一個很常用的場景是在一個公司的項目中,經常由架構師搭好架構,聲明出抽象方法。下面的程序員再去分頭重寫這些抽象方法。

在深入了解之前,容許我先扯遠一點。
作為一個進化論的反對者,假設這個世界是上帝用代碼創造的。那麼上帝創造生命的時候可能就用到了模版方法模式。看看他是怎麼在生命構造器中聲明模版方法的:

var Life = function(){
}
Life.prototype.init = function(){
this.DNA復制();
this.出生();
this.成長();
this.衰老();
this.死亡();
}
this.prototype.DNA復制 = function(){
&*$%&^%^&(&(&(&&(^^(*) 
//看不懂的代碼
}
Life.prototype.出生 = function(){
}
Life.prototype.成長 = function(){
}
Life.prototype.衰老 = function(){
}
Life.prototype.死亡 = function(){
}

其中DNA復制是預先定義的算法中不變部分. 所有子類都不能改寫它. 如果需要我們可以寫成protected的類型.
而其他的函數在父類中會被先定義成一個空函數(鉤子). 然後被子類重寫,這就是模版方法中所謂的可變的步驟。
假設有個子類哺乳動物類繼承了Life類.

var Mammal = function(){
}
Mammal.prototype = Life.prototype; 
//繼承Life

然後重寫出生和衰老這兩個鉤子函數.

Mammal.prototope.出生 = function(){
'胎生()
}
Mammal.prototype.成長 = function(){
//再留給子類去實現
}
Mammal.prototope.衰老 = function(){
自由基的過氧化反應()
}
Life.prototype.死亡 = function(){
//再留給子類去實現
}
//再實現一個Dog類
var = Dog = function(){
}
//Dog繼承自哺乳動物.
Dog.prototype = Mammal.prototype;
var dog = new Dog();
dog.init();

至此,一只小狗的生命會依次經歷DNA復制,出生,成長,衰老,死亡這幾個過程。這些步驟早在它出生前就決定了。所幸的是,上帝沒有安排好它生命的所有細節。它還是能通過對成長函數的重寫,來成為一只與眾不同的小狗。

舉個稍微現實點的例子,游戲大廳中的所有游戲都有登錄,游戲中,游戲結束這幾個過程,而登錄和游戲結束之後彈出提示這些函數都是應該公用的。
那麼首先需要的是一個父類。

var gameCenter = function(){
}
gameCenter.ptototype.init = function(){
this.login();
this.gameStart();
this.end();
}
gameCenter.prototype.login= function(){
//do something
}
gameCenter.prototype.gameStart= function(){
//空函數, 留給子類去重寫
}
gameCenter.prototype.end= function(){
alert ( "歡迎下次再來玩" );
}

接下來創建一個斗地主的新游戲, 只需要繼承gameCenter然後重寫它的gameStart函數.

var 斗地主 = function(){
}
斗地主.prototype = gameCenter.prototype; 
//繼承
斗地主.prototype.gameStart = function(){
//do something
}
(new 斗地主).init();

這樣一局新的游戲就開始了.

十一 中介者模式

中介者對象可以讓各個對象之間不需要顯示的相互引用,從而使其耦合松散,而且可以獨立的改變它們之間的交互。

打個比方,軍火買賣雙方為了安全起見,找了一個信任的中介來進行交易。買家A把錢交給中介B,然後從中介手中得到軍火,賣家C把軍火賣給中介,然後從中介手中拿回錢。一場交易完畢,A甚至不知道C是一只猴子還是一只猛犸。因為中介的存在,A也未必一定要買C的軍火,也可能是D,E,F。

銀行在存款人和貸款人之間也能看成一個中介。存款人A並不關心他的錢最後被誰借走。貸款人B也不關心他借來的錢來自誰的存款。因為有中介的存在,這場交易才變得如此方便。

中介者模式和代理模式有一點點相似。都是第三者對象來連接2個對象的通信。具體差別可以從下圖中區別。

代理模式:

中介者模式

代理模式中A必然是知道B的一切,而中介者模式中A,B,C對E,F,G的實現並不關心.而且中介者模式可以連接任意多種對象。

切回到程序世界裡的mvc,無論是j2ee中struts的Action. 還是js中backbone.js和spine.js裡的Controler. 都起到了一個中介者的作用.
拿backbone舉例. 一個mode裡的數據並不確定最後被哪些view使用. view需要的數據也可以來自任意一個mode. 所有的綁定關系都是在controler裡決定. 中介者把復雜的多對多關系, 變成了2個相對簡單的1對多關系.

一段簡單的示例代碼:

var mode1 = Mode.create(), mode2 = Mode.create();
var view1 = View.create(), view2 = View.create();
var controler1 = Controler.create( mode1, view1, function(){
view1.el.find( ''div' ).bind( ''click', function(){
this.innerHTML = mode1.find( 'data' );
} )
})
var controler2 = Controler.create( mode2 view2, function(){
view1.el.find( ''div' ).bind( ''click', function(){
this.innerHTML = mode2.find( 'data' );
} )
})

十二 迭代器模式

迭代器模式提供一種方法順序訪問一個聚合對象中各個元素,而又不需要暴露該方法中的內部表示。
js中我們經常會封裝一個each函數用來實現迭代器。

array的迭代器:

forEach = function( ary, fn ){ for ( var i = 0, l = ary.length; i < l; i++ ){ var c = ary[ i ]; if ( fn.call( c, i , c ) === false ){ return false; } }}
forEach( [ 1, 2, 3 ], function( i, n ){
alert ( i );
})

obejct的迭代器:

forEach = function( obj, fn ){ for ( var i in obj ){ var c = obj[ i ]; if ( fn.call( c, i, c ) === false ){ return false; } }}
forEach( {"a": 1,"b": 2}, function( i, n ){
alert ( i );
})

十三 組合模式

組合模式又叫部分-整體模式,它將所有對象組合成樹形結構。使得用戶只需要操作最上層的接口,就可以對所有成員做相同的操作。

一個再好不過的例子就是jquery對象,大家都知道1個jquery對象其實是一組對象集合。比如在這樣一個HTML頁面

<div>
<span></span>
<span></span>
</div>

我們想取消所有節點上綁定的事件, 需要這樣寫

var allNodes = document.getElementsByTagName("*");
var len = allNodes.length;
while( len-- ){
allNodes.unbind("*");
}

但既然用了jquery,就肯定不會再做這麼搓的事情。我們只需要$( ‘body' ).unbind( ‘*' );
當每個元素都實現unbind接口, 那麼只需調用最上層對象$( ‘body' )的unbind, 便可自動迭代並調用所有組合元素的unbind方法.
再來個具體點的例子, 還是dev.qplus.com這個網站的即時驗證表單。

注意下面那個修改資料的按鈕,如果有任意一個field的驗證沒有通過,修改資料的按鈕都將是灰色不可點的狀態。 這意味著我們重新填寫了表單內容後, 都得去校驗每個field, 保證它們全部OK.
這代碼不難實現.

if ( nameField.validata() && idCard.validata() && email.validata() && phone.validata() ){
alert ( "驗證OK" );
}

似乎我們用一個外觀模式也能勉強解決這裡條件分支堆砌的問題,但真正的問題是,我們並不能保證表單裡field的數量,也許明天產品經理就讓你刪掉一個或者增加兩個.那麼這樣的維護方式顯然不能被接受.
更好的實現是有一個form.validata函數, 它負責把真正的validata操作分發給每個組合對象.
form.validata函數裡面會依次遍歷所有需要校驗的field. 若有一個field校驗未通過, form.validata都會返回false. 偽代碼如下.

form.validata = function(){
forEach( fields, function( index, field ){
if ( field.validata() === false ){
return false;
}
})
return true;
}

十四 備忘錄模式

備忘錄模式在js中經常用於數據緩存. 比如一個分頁控件, 從服務器獲得某一頁的數據後可以存入緩存。以後再翻回這一頁的時候,可以直接使用緩存裡的數據而無需再次請求服務器。
實現比較簡單,偽代碼:

var Page = function(){
var page = 1,
cache = {},
data;
return function( page ){
if ( cache[ page ] ){
data = cache[ page ];
render( data );
}else{
Ajax.send( 'cgi.xx.com/xxx', function( data ){
cache[ page ] = data;
render( data );
})
}
}
}()

十五 職責鏈模式

職責鏈模式是一個對象A向另一個對象B發起請求,如果B不處理,可以把請求轉給C,如果C不處理,又可以把請求轉給D。一直到有一個對象願意處理這個請求為止。

打個比方,客戶讓老板寫個php程序。老板肯定不寫,然後老板交給了部門經理。部門經理不願意寫,又交給項目經理。項目經理不會寫,又交給程序員。最後由碼農來完成。

在這個假設裡, 有幾條職責鏈模式的特點。

1 老板只跟部門經理打交道,部門經理只聯系項目經理,項目經理只找碼農的麻煩。
2 如果碼農也不寫,這個項目將會流產。
3 客戶並不清楚這個程序最後是由誰寫出來的。
js中的事件冒泡就是作為一個職責鏈來實現的。一個事件在某個節點上被觸發,然後向根節點傳遞, 直到被節點捕獲。

十六 享元模式

享元模式主要用來減少程序所需的對象個數. 有一個例子, 我們這邊的前端同學幾乎人手一本《javascript權威指南》. 從省錢的角度講, 大約三本就夠了. 放在部門的書櫃裡, 誰需要看的時候就去拿, 看完了還回去. 如果同時有4個同學需要看, 此時再去多買一本.

在webqq裡面, 打開QQ好友列表往下拉的時候,會為每個好友創建一個div( 如果算上div中的子節點, 還遠不只1個元素 ).

如果有1000個QQ好友, 意味著如果從頭拉到尾, 會創建1000個div, 這時候有些浏覽器也許已經假死了. 這還只是一個隨便翻翻好友列表的操作.

所以我們想到了一種解決辦法, 當滾動條滾動的時候, 把已經消失在視線外的div都刪除掉. 這樣頁面可以保持只有一定數量的節點. 問題是這樣頻繁的添加與刪除節點, 也會造成很大的性能開銷, 而且這種感覺很不對味.

現在享元模式可以登場了. 顧名思義, 享元模式可以提供一些共享的對象以便重復利用. 仔細看下上圖, 其實我們一共只需要10個div來顯示好友信息,也就是出現在用戶視線中的10個div.這10個div就可以寫成享元.
偽代碼如下.

var getDiv = (function(){
var created = [];
var create = function(){
return document.body.appendChild( document.createElement( 'div' ) );
}
var get = function(){
if ( created.length ){
return created.shift();
}else{
return create();
}
}
/* 一個假設的事件,用來監聽剛消失在視線外的div,實際上可以通過監聽滾 動條位置來實現 */
userInfoContainer.disappear(function( div ){
created.push( div );
})
})()
var div = getDiv();
div.innerHTML = "${userinfo}";

原理其實很簡單, 把剛隱藏起來的div放到一個數組中, 當需要div的時候, 先從該數組中取, 如果數組中已經沒有了, 再重新創建一個. 這個數組裡的div就是享元, 它們每一個都可以當作任何用戶信息的載體.

當然這只是個示例,實際的情況要復雜一些, 比如快速拖動的時候, 我們可能還得為節點設置一個緩沖區.

十七 狀態模式

狀態模式主要可以用於這種場景
1 一個對象的行為取決於它的狀態
2 一個操作中含有龐大的條件分支語句

回想下街頭霸王的游戲。

隆有走動,攻擊,防御,跌倒,跳躍等等多種狀態,而這些狀態之間既有聯系又互相約束。比如跳躍的時候是不能攻擊和防御的。跌倒的時候既不能攻擊又不能防御,而走動的時候既可以攻擊也可以跳躍。

要完成這樣一系列邏輯, 常理下if else是少不了的. 而且數量無法估計, 特別是增加一種新狀態的時候, 可能要從代碼的第10行一直改到900行.

if ( state === 'jump' ){
if ( currState === 'attack' || currState === 'defense' ){
return false;
}
}else if ( state === 'wait' ){
if ( currState === 'attack' || currState === 'defense' ){
return true;
}
}

為了消滅這些if else, 並且方便修改和維護, 我們引入一個狀態類.

var StateManager = function(){
var currState = 'wait';
var states = {
jump: function( state ){
},
wait: function( state ){
},
attack: function( state ){
},
crouch: function( state ){
},
defense: function( state ){
if ( currState === 'jump' ){
return false; 
//不成功,跳躍的時候不能防御
}
//do something; //防御的真正邏輯代碼, 為了防止狀態類的代碼過多, 應該把這些邏輯繼續扔給真正的fight類來執行.
currState = 'defense'; 
// 切換狀態
}
}
var changeState = function( state ){
states[ state ] && states[ state ]();
}
return {
changeState : changeState
}
}
var stateManager = StateManager();
stateManager.changeState( 'defense' );

通過這個狀態類,可以把散落在世界各地的條件分支集中管理到一個類裡,並且可以很容易的添加一種新的狀態。而作為調用者,只需要通過暴露的changeState接口來切換人物的狀態。

/***************************分界線1******************************************/

GOF提出的23種設計模式,至此已經寫完大半。還有一些要麼是js裡不太適用,要麼是js中已有原生自帶的實現,所以就沒再去深究。這2篇文章裡的大部分例子都來自或改寫自工作和學習中的代碼。我對設計模式的看法是不用刻意去學習設計模式,平時我們接觸的很多代碼裡已經包含了一些設計模式的實現。我的過程是讀過prototype和jquery的源碼後,回頭翻設計模式的書,發現不知覺中已經接觸過十之六七。

同樣在實際的編碼中也沒有必要刻意去使用一些設計模式。就如同tokyo hot 32式一樣,在一場友好的papapa過程中,沒有必要去刻意使用某種姿勢。一切還是看需求和感覺。

XML學習教程| jQuery入門知識| AJAX入門| Dreamweaver教程| Fireworks入門知識| SEO技巧| SEO優化集錦|
Copyright © DIV+CSS佈局教程網 All Rights Reserved