欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

常用的Javascript設(shè)計(jì)模式小結(jié)

 更新時(shí)間:2015年12月09日 14:02:38   投稿:mrr  
javascript設(shè)計(jì)模式有很多種,本文給大家介紹常用的javascript設(shè)計(jì)模式,對(duì)javascript設(shè)計(jì)模式相關(guān)知識(shí)感興趣的朋友一起學(xué)習(xí)吧

《Practical Common Lisp》的作者 Peter Seibel 曾說,如果你需要一種模式,那一定是哪里出了問題。他所說的問題是指因?yàn)檎Z言的天生缺陷,不得不去尋求和總結(jié)一種通用的解決方案。

不管是弱類型或強(qiáng)類型,靜態(tài)或動(dòng)態(tài)語言,命令式或說明式語言、每種語言都有天生的優(yōu)缺點(diǎn)。一個(gè)牙買加運(yùn)動(dòng)員, 在短跑甚至拳擊方面有一些優(yōu)勢(shì),在練瑜伽上就欠缺一些。

術(shù)士和暗影牧師很容易成為一個(gè)出色的輔助,而一個(gè)背著梅肯滿地圖飛的敵法就會(huì)略顯尷尬。 換到程序中, 靜態(tài)語言里可能需要花很多功夫來實(shí)現(xiàn)裝飾者,而js由于能隨時(shí)往對(duì)象上面扔方法,以至于裝飾者模式在js里成了雞肋。

講 Javascript 設(shè)計(jì)模式的書還比較少,《Pro javaScript Design Patterns》是比較經(jīng)典的一本,但是它里面的例子舉得比較啰嗦,所以結(jié)合我在工作中寫過的代碼,把我的理解總結(jié)一下。如果我的理解出現(xiàn)了偏差,請(qǐng)不吝指正。

一 單例模式

單例模式的定義是產(chǎn)生一個(gè)類的唯一實(shí)例,但js本身是一種“無類”語言。很多講js設(shè)計(jì)模式的文章把{}當(dāng)成一個(gè)單例來使用也勉強(qiáng)說得通。因?yàn)閖s生成對(duì)象的方式有很多種,我們來看下另一種更有意義的單例。

有這樣一個(gè)常見的需求,點(diǎn)擊某個(gè)按鈕的時(shí)候需要在頁面彈出一個(gè)遮罩層。比如web.qq.com點(diǎn)擊登錄的時(shí)候.

 

這個(gè)生成灰色背景遮罩層的代碼是很好寫的.

var createMask = function(){
return document,body.appendChild( document.createElement(div) );
}
$( 'button' ).click( function(){
Var mask = createMask();
mask.show();
})

問題是, 這個(gè)遮罩層是全局唯一的, 那么每次調(diào)用createMask都會(huì)創(chuàng)建一個(gè)新的div, 雖然可以在隱藏遮罩層的把它remove掉. 但顯然這樣做不合理.

再看下第二種方案, 在頁面的一開始就創(chuàng)建好這個(gè)div. 然后用一個(gè)變量引用它.

var mask = document.body.appendChild( document.createElement( ''div' ) );
$( ''button' ).click( function(){
mask.show();
} )

這樣確實(shí)在頁面只會(huì)創(chuàng)建一個(gè)遮罩層div, 但是另外一個(gè)問題隨之而來, 也許我們永遠(yuǎn)都不需要這個(gè)遮罩層, 那又浪費(fèi)掉一個(gè)div, 對(duì)dom節(jié)點(diǎn)的任何操作都應(yīng)該非常吝嗇.

如果可以借助一個(gè)變量. 來判斷是否已經(jīng)創(chuàng)建過div呢?

var mask;
var createMask = function(){
if ( mask ) return mask;
else{
mask = document,body.appendChild( document.createElement(div) );
return mask;
}
}

看起來不錯(cuò), 到這里的確完成了一個(gè)產(chǎn)生單列對(duì)象的函數(shù). 我們?cè)僮屑?xì)看這段代碼有什么不妥.

首先這個(gè)函數(shù)是存在一定副作用的, 函數(shù)體內(nèi)改變了外界變量mask的引用, 在多人協(xié)作的項(xiàng)目中, createMask是個(gè)不安全的函數(shù). 另一方面, mask這個(gè)全局變量并不是非需不可. 再來改進(jìn)一下.

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

用了個(gè)簡(jiǎn)單的閉包把變量mask包起來, 至少對(duì)于createMask函數(shù)來講, 它是封閉的.

可能看到這里, 會(huì)覺得單例模式也太簡(jiǎn)單了. 的確一些設(shè)計(jì)模式都是非常簡(jiǎn)單的, 即使從沒關(guān)注過設(shè)計(jì)模式的概念, 在平時(shí)的代碼中也不知不覺用到了一些設(shè)計(jì)模式. 就像多年前我明白老漢推車是什么回事的時(shí)候也想過尼瑪原來這就是老漢推車.

GOF里的23種設(shè)計(jì)模式, 也是在軟件開發(fā)中早就存在并反復(fù)使用的模式. 如果程序員沒有明確意識(shí)到他使用過某些模式, 那么下次他也許會(huì)錯(cuò)過更合適的設(shè)計(jì) (這段話來自《松本行弘的程序世界》).

再回來正題, 前面那個(gè)單例還是有缺點(diǎn). 它只能用于創(chuàng)建遮罩層. 假如我又需要寫一個(gè)函數(shù), 用來創(chuàng)建一個(gè)唯一的xhr對(duì)象呢? 能不能找到一個(gè)通用的singleton包裝器.

js中函數(shù)是第一型, 意味著函數(shù)也可以當(dāng)參數(shù)傳遞. 看看最終的代碼.

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') );
})

用一個(gè)變量來保存第一次的返回值, 如果它已經(jīng)被賦值過, 那么在以后的調(diào)用中優(yōu)先返回該變量. 而真正創(chuàng)建遮罩層的代碼是通過回調(diào)函數(shù)的方式傳人到singleton包裝器中的. 這種方式其實(shí)叫橋接模式. 關(guān)于橋接模式, 放在后面一點(diǎn)點(diǎn)來說.

然而singleton函數(shù)也不是完美的, 它始終還是需要一個(gè)變量result來寄存div的引用. 遺憾的是js的函數(shù)式特性還不足以完全的消除聲明和語句.

二 簡(jiǎn)單工廠模式

簡(jiǎn)單工廠模式是由一個(gè)方法來決定到底要?jiǎng)?chuàng)建哪個(gè)類的實(shí)例, 而這些實(shí)例經(jīng)常都擁有相同的接口. 這種模式主要用在所實(shí)例化的類型在編譯期并不能確定, 而是在執(zhí)行期決定的情況。 說的通俗點(diǎn),就像公司茶水間的飲料機(jī),要咖啡還是牛奶取決于你按哪個(gè)按鈕。

簡(jiǎn)單工廠模式在創(chuàng)建ajax對(duì)象的時(shí)候也非常有用.

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

這個(gè)庫里提供了幾種ajax請(qǐng)求的方式,包括xhr對(duì)象的get, post, 也包括跨域用的jsonp和iframe. 為了方便使用, 這幾種方式都抽象到了同一個(gè)接口里面.

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實(shí)際上就是一個(gè)工廠方法, 至于到底是產(chǎn)生xhr的實(shí)例, 還是jsonp的實(shí)例. 是由后來的代碼決定的。

實(shí)際上在js里面,所謂的構(gòu)造函數(shù)也是一個(gè)簡(jiǎn)單工廠。只是批了一件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和構(gòu)造器的相關(guān)說明, 可以看到,所謂的new, 本身只是一個(gè)對(duì)象的復(fù)制和改寫過程, 而具體會(huì)生成什么是由調(diào)用ObjectFactory時(shí)傳進(jìn)去的參數(shù)所決定的。

三 觀察者模式

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

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

只要訂閱了div的click事件. 當(dāng)點(diǎn)擊div的時(shí)候, function click就會(huì)被觸發(fā).

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

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

再舉個(gè)例子,我來公司面試的時(shí)候,完事之后每個(gè)面試官都會(huì)對(duì)我說:“請(qǐng)留下你的聯(lián)系方式, 有消息我們會(huì)通知你”。 在這里“我”是訂閱者, 面試官是發(fā)布者。所以我不用每天或者每小時(shí)都去詢問面試結(jié)果, 通訊的主動(dòng)權(quán)掌握在了面試官手上。而我只需要提供一個(gè)聯(lián)系方式。

觀察者模式可以很好的實(shí)現(xiàn)2個(gè)模塊之間的解耦。 假如我正在一個(gè)團(tuán)隊(duì)里開發(fā)一個(gè)html5游戲. 當(dāng)游戲開始的時(shí)候,需要加載一些圖片素材。加載好這些圖片之后開始才執(zhí)行游戲邏輯. 假設(shè)這是一個(gè)需要多人合作的項(xiàng)目. 我完成了Gamer和Map模塊, 而我的同事A寫了一個(gè)圖片加載器loadImage.

loadImage的代碼如下

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

當(dāng)圖片加載好之后, 再渲染地圖, 執(zhí)行游戲邏輯. 嗯, 這個(gè)程序運(yùn)行良好. 突然有一天, 我想起應(yīng)該給游戲加上聲音功能. 我應(yīng)該讓圖片加載器添上一行代碼.

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

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

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

loadImage完成之后, 它根本不關(guān)心將來會(huì)發(fā)生什么, 因?yàn)樗墓ぷ饕呀?jīng)完成了. 接下來它只要發(fā)布一個(gè)信號(hào).

loadImage.trigger( ”ready' );

那么監(jiān)聽了loadImage的'ready'事件的對(duì)象都會(huì)收到通知. 就像上個(gè)面試的例子. 面試官根本不關(guān)心面試者們收到面試結(jié)果后會(huì)去哪吃飯. 他只負(fù)責(zé)把面試者的簡(jiǎn)歷搜集到一起. 當(dāng)面試結(jié)果出來時(shí)照著簡(jiǎn)歷上的電話挨個(gè)通知.

說了這么多概念, 來一個(gè)具體的實(shí)現(xiàn). 實(shí)現(xiàn)過程其實(shí)很簡(jiǎn)單. 面試者把簡(jiǎn)歷扔到一個(gè)盒子里, 然后面試官在合適的時(shí)機(jī)拿著盒子里的簡(jiǎn)歷挨個(gè)打電話通知結(jié)果.

Events = function() {
var listen, log, obj, one, remove, trigger, __this;
obj = {};
__this = this;
listen = function( key, eventfn ) { 
//把簡(jiǎn)歷扔盒子, key就是聯(lián)系方式.
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
}
}

最后用觀察者模式來做一個(gè)成人電視臺(tái)的小應(yīng)用.

//訂閱者

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

四 適配器模式

去年年前當(dāng)時(shí)正在開發(fā)dev.qplus.com, 有個(gè)存儲(chǔ)應(yīng)用分類id的js文件, 分類id的結(jié)構(gòu)最開始設(shè)計(jì)的比較笨重. 于是我決定重構(gòu)它. 我把它定義成一個(gè)json樹的形式, 大概是這樣:

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

dev.qplus.com里大概有4,5個(gè)頁面都調(diào)用這個(gè)category對(duì)象. 春節(jié)前我休了1個(gè)星期假. 過年來之后發(fā)現(xiàn)郵箱里有封郵件, 設(shè)計(jì)數(shù)據(jù)庫的同學(xué)把category..js也重構(gòu)了一份, 并且其他幾個(gè)項(xiàng)目里都是用了這份category.js, 我拿過來一看就傻眼了, 和我之前定的數(shù)據(jù)結(jié)構(gòu)完全不一樣.

當(dāng)然這是一個(gè)溝通上的反面例子. 但接下來的重點(diǎn)是我已經(jīng)在N個(gè)文件里用到了之前我定的category.js. 而且惹上了一些復(fù)雜的相關(guān)邏輯. 怎么改掉我之前的代碼呢. 全部重寫肯定是不愿意. 所以現(xiàn)在適配器就派上用場(chǎng)了.

只需要把同事的category用一個(gè)函數(shù)轉(zhuǎn)成跟我之前定義的一樣.

my.category = adapterCategory ( afu.category );

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

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

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

五 代理模式

代理模式的定義是把對(duì)一個(gè)對(duì)象的訪問, 交給另一個(gè)代理對(duì)象來操作.

舉一個(gè)例子, 我在追一個(gè)MM想給她送一束花,但是我因?yàn)槲倚愿癖容^靦腆,所以我托付了MM的一個(gè)好朋友來送。

這個(gè)例子不是非常好, 至少我們沒看出代理模式有什么大的用處,因?yàn)樽稭M更好的方式是送一臺(tái)寶馬。

再舉個(gè)例子,假如我每天都得寫工作日?qǐng)?bào)( 其實(shí)沒有這么慘 ). 我的日?qǐng)?bào)最后會(huì)讓總監(jiān)審閱. 如果我們都直接把日?qǐng)?bào)發(fā)給 總監(jiān) , 那可能 總監(jiān) 就沒法工作了. 所以通常的做法是把日?qǐng)?bào)發(fā)給我的組長(zhǎng) , 組長(zhǎng)把所有組員一周的日?qǐng)?bào)都匯總后再發(fā)給總監(jiān) .

實(shí)際的編程中, 這種因?yàn)樾阅軉栴}使用代理模式的機(jī)會(huì)是非常多的。比如頻繁的訪問dom節(jié)點(diǎn), 頻繁的請(qǐng)求遠(yuǎn)程資源. 可以把操作先存到一個(gè)緩沖區(qū), 然后自己選擇真正的觸發(fā)時(shí)機(jī).

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

 

游戲中隆需要接受鍵盤的事件, 來完成相應(yīng)動(dòng)作.

于是我寫了一個(gè)keyManage類. 其中在游戲主線程里監(jiān)聽keyManage的變化.

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

圖片里面隆正在放升龍拳, 升龍拳的操作是前下前+拳. 但是這個(gè)keyManage類只要發(fā)生鍵盤事件就會(huì)觸發(fā)之前監(jiān)聽的change函數(shù). 這意味著永遠(yuǎn)只能取得前,后,前,拳這樣單獨(dú)的按鍵事件,而無法得到一個(gè)按鍵組合。

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

所以最后的keyManage只負(fù)責(zé)映射鍵盤事件. 而隆接受到的動(dòng)作是通過一個(gè)代理對(duì)象處理之后的.

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

至于proxy里面怎么實(shí)現(xiàn),完全可以自由發(fā)揮。

還有個(gè)例子就是在調(diào)用ajax請(qǐng)求的時(shí)候,無論是各種開源庫,還是自己寫的Ajax類, 都會(huì)給xhr對(duì)象設(shè)置一個(gè)代理. 我們不可能頻繁的去操作xhr對(duì)象發(fā)請(qǐng)求, 而應(yīng)該是這樣.

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

六 橋接模式

橋接模式的作用在于將實(shí)現(xiàn)部分和抽象部分分離開來, 以便兩者可以獨(dú)立的變化。在實(shí)現(xiàn)api的時(shí)候, 橋接模式特別有用。比如最開始的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是實(shí)現(xiàn)部分。 他們完全可以獨(dú)自變化互不影響。 如果需要再寫一個(gè)單例的createScript就一點(diǎn)也不費(fèi)力.

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

另外一個(gè)常見的例子就是forEach函數(shù)的實(shí)現(xiàn), 用來迭代一個(gè)數(shù)組.

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函數(shù)并不關(guān)心fn里面的具體實(shí)現(xiàn). fn里面的邏輯也不會(huì)被forEach函數(shù)的改寫影響.

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

七 外觀模式

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

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

如果你需要分別調(diào)用getName和getSex函數(shù). 那可以用一個(gè)更高層的接口getUserInfo來調(diào)用.

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

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

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

答案是顯而易見的,飯?zhí)玫某床藥煾挡粫?huì)因?yàn)槟泐A(yù)定了一份燒鴨和一份白菜就把這兩樣菜炒在一個(gè)鍋里。他更愿意給你提供一個(gè)燒鴨飯?zhí)撞?。同樣在程序設(shè)計(jì)中,我們需要保證函數(shù)或者對(duì)象盡可能的處在一個(gè)合理粒度,畢竟不是每個(gè)人喜歡吃燒鴨的同時(shí)又剛好喜歡吃白菜。
外觀模式還有一個(gè)好處是可以對(duì)用戶隱藏真正的實(shí)現(xiàn)細(xì)節(jié),用戶只關(guān)心最高層的接口。比如在燒鴨飯?zhí)撞偷墓适轮校悴⒉魂P(guān)心師傅是先做燒鴨還是先炒白菜,你也不關(guān)心那只鴨子是在哪里成長(zhǎng)的。

最后寫個(gè)我們都用過的外觀模式例子

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

八 訪問者模式

GOF官方定義: 訪問者模式是表示一個(gè)作用于某個(gè)對(duì)象結(jié)構(gòu)中的各元素的操作。它使可以在不改變各元素的類的前提下定義作用于這些元素的新操作。我們?cè)谑褂靡恍┎僮鲗?duì)不同的對(duì)象進(jìn)行處理時(shí),往往會(huì)根據(jù)不同的對(duì)象選擇不同的處理方法和過程。在實(shí)際的代碼過程中,我們可以發(fā)現(xiàn),如果讓所有的操作分散到各個(gè)對(duì)象中,整個(gè)系統(tǒng)會(huì)變得難以維護(hù)和修改。且增加新的操作通常都要重新編譯所有的類。因此,為了解決這個(gè)問題,我們可以將每一個(gè)類中的相關(guān)操作提取出來,包裝成一個(gè)獨(dú)立的對(duì)象,這個(gè)對(duì)象我們就稱為訪問者(Visitor)。利用訪問者,對(duì)訪問的元素進(jìn)行某些操作時(shí),只需將此對(duì)象作為參數(shù)傳遞給當(dāng)前訪問者,然后,訪問者會(huì)依據(jù)被訪問者的具體信息,進(jìn)行相關(guān)的操作。

據(jù)統(tǒng)計(jì),上面這段話只有5%的人會(huì)看到最后一句。那么通俗點(diǎn)講,訪問者模式先把一些可復(fù)用的行為抽象到一個(gè)函數(shù)(對(duì)象)里,這個(gè)函數(shù)我們就稱為訪問者(Visitor)。如果另外一些對(duì)象要調(diào)用這個(gè)函數(shù),只需要把那些對(duì)象當(dāng)作參數(shù)傳給這個(gè)函數(shù),在js里我們經(jīng)常通過call或者apply的方式傳遞this對(duì)象給一個(gè)Visitor函數(shù).
訪問者模式也被稱為GOF總結(jié)的23種設(shè)計(jì)模式中最難理解的一種。不過這有很大一部分原因是因?yàn)椤对O(shè)計(jì)模式》基于C++和Smalltalk寫成. 在強(qiáng)類型語言中需要通過多次重載來實(shí)現(xiàn)訪問者的接口匹配。

而在js這種基于鴨子類型的語言中,訪問者模式幾乎是原生的實(shí)現(xiàn), 所以我們可以利用apply和call毫不費(fèi)力的使用訪問者模式,這一小節(jié)更關(guān)心的是這種模式的思想以及在js引擎中的實(shí)現(xiàn)。

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

這個(gè)就是鴨子類型的概念,在js這種弱類型語言里,很多方法里都不做對(duì)象的類型檢測(cè),而是只關(guān)心這些對(duì)象能做什么。
Array構(gòu)造器和String構(gòu)造器的prototype上的方法就被特意設(shè)計(jì)成了訪問者。這些方法不對(duì)this的數(shù)據(jù)類型做任何校驗(yàn)。這也就是為什么arguments能冒充array調(diào)用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方法沒有對(duì)this的類型做任何顯示的限制,所以理論上任何對(duì)象都可以被傳入ArrayPush這個(gè)訪問者。

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

1、 this對(duì)象上面可儲(chǔ)存屬性. //反例: 值類型的數(shù)據(jù)
2、 this的length屬性可寫. //反例: functon對(duì)象, function有一個(gè)只讀的length屬性, 表示形參個(gè)數(shù).

如果不符合這2條規(guī)則的話,代碼在執(zhí)行期會(huì)報(bào)錯(cuò). 也就是說, Array.prototype.push.call( 1, ‘first' )和Array.prototoype.push.call( function(){}, ‘first' )都達(dá)不到預(yù)期的效果.

利用訪問者,我們來做個(gè)有趣的事情. 給一個(gè)object對(duì)象增加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

九 策略模式

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

回憶下jquery里的animate方法.

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

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

 

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

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

可以看到,各種驗(yàn)證規(guī)則很容易被修改和相互替換。如果某天產(chǎn)品經(jīng)理建議字符過長(zhǎng)的限制改成60個(gè)字符。那只需要0.5秒完成這次工作。

十 模版方法模式

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

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

在深入了解之前,容許我先扯遠(yuǎn)一點(diǎn)。
作為一個(gè)進(jìn)化論的反對(duì)者,假設(shè)這個(gè)世界是上帝用代碼創(chuàng)造的。那么上帝創(chuàng)造生命的時(shí)候可能就用到了模版方法模式??纯此窃趺丛谏鼧?gòu)造器中聲明模版方法的:

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

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

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

然后重寫出生和衰老這兩個(gè)鉤子函數(shù).

Mammal.prototope.出生 = function(){
'胎生()
}
Mammal.prototype.成長(zhǎng) = function(){
//再留給子類去實(shí)現(xiàn)
}
Mammal.prototope.衰老 = function(){
自由基的過氧化反應(yīng)()
}
Life.prototype.死亡 = function(){
//再留給子類去實(shí)現(xiàn)
}
//再實(shí)現(xiàn)一個(gè)Dog類
var = Dog = function(){
}
//Dog繼承自哺乳動(dòng)物.
Dog.prototype = Mammal.prototype;
var dog = new Dog();
dog.init();

至此,一只小狗的生命會(huì)依次經(jīng)歷DNA復(fù)制,出生,成長(zhǎng),衰老,死亡這幾個(gè)過程。這些步驟早在它出生前就決定了。所幸的是,上帝沒有安排好它生命的所有細(xì)節(jié)。它還是能通過對(duì)成長(zhǎng)函數(shù)的重寫,來成為一只與眾不同的小狗。

舉個(gè)稍微現(xiàn)實(shí)點(diǎn)的例子,游戲大廳中的所有游戲都有登錄,游戲中,游戲結(jié)束這幾個(gè)過程,而登錄和游戲結(jié)束之后彈出提示這些函數(shù)都是應(yīng)該公用的。
那么首先需要的是一個(gè)父類。

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

接下來創(chuàng)建一個(gè)斗地主的新游戲, 只需要繼承g(shù)ameCenter然后重寫它的gameStart函數(shù).

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

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

十一 中介者模式

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

打個(gè)比方,軍火買賣雙方為了安全起見,找了一個(gè)信任的中介來進(jìn)行交易。買家A把錢交給中介B,然后從中介手中得到軍火,賣家C把軍火賣給中介,然后從中介手中拿回錢。一場(chǎng)交易完畢,A甚至不知道C是一只猴子還是一只猛犸。因?yàn)橹薪榈拇嬖?,A也未必一定要買C的軍火,也可能是D,E,F(xiàn)。

銀行在存款人和貸款人之間也能看成一個(gè)中介。存款人A并不關(guān)心他的錢最后被誰借走。貸款人B也不關(guān)心他借來的錢來自誰的存款。因?yàn)橛兄薪榈拇嬖?,這場(chǎng)交易才變得如此方便。

中介者模式和代理模式有一點(diǎn)點(diǎn)相似。都是第三者對(duì)象來連接2個(gè)對(duì)象的通信。具體差別可以從下圖中區(qū)別。

代理模式:

中介者模式

代理模式中A必然是知道B的一切,而中介者模式中A,B,C對(duì)E,F,G的實(shí)現(xiàn)并不關(guān)心.而且中介者模式可以連接任意多種對(duì)象。

切回到程序世界里的mvc,無論是j2ee中struts的Action. 還是js中backbone.js和spine.js里的Controler. 都起到了一個(gè)中介者的作用.
拿backbone舉例. 一個(gè)mode里的數(shù)據(jù)并不確定最后被哪些view使用. view需要的數(shù)據(jù)也可以來自任意一個(gè)mode. 所有的綁定關(guān)系都是在controler里決定. 中介者把復(fù)雜的多對(duì)多關(guān)系, 變成了2個(gè)相對(duì)簡(jiǎn)單的1對(duì)多關(guān)系.

一段簡(jiǎn)單的示例代碼:

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' );
} )
})

十二 迭代器模式

迭代器模式提供一種方法順序訪問一個(gè)聚合對(duì)象中各個(gè)元素,而又不需要暴露該方法中的內(nèi)部表示。
js中我們經(jīng)常會(huì)封裝一個(gè)each函數(shù)用來實(shí)現(xiàn)迭代器。

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 );
})

十三 組合模式

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

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

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

我們想取消所有節(jié)點(diǎn)上綁定的事件, 需要這樣寫

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

但既然用了jquery,就肯定不會(huì)再做這么搓的事情。我們只需要$( ‘body' ).unbind( ‘*' );
當(dāng)每個(gè)元素都實(shí)現(xiàn)unbind接口, 那么只需調(diào)用最上層對(duì)象$( ‘body' )的unbind, 便可自動(dòng)迭代并調(diào)用所有組合元素的unbind方法.
再來個(gè)具體點(diǎn)的例子, 還是dev.qplus.com這個(gè)網(wǎng)站的即時(shí)驗(yàn)證表單。

注意下面那個(gè)修改資料的按鈕,如果有任意一個(gè)field的驗(yàn)證沒有通過,修改資料的按鈕都將是灰色不可點(diǎn)的狀態(tài)。 這意味著我們重新填寫了表單內(nèi)容后, 都得去校驗(yàn)每個(gè)field, 保證它們?nèi)縊K.
這代碼不難實(shí)現(xiàn).

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

似乎我們用一個(gè)外觀模式也能勉強(qiáng)解決這里條件分支堆砌的問題,但真正的問題是,我們并不能保證表單里field的數(shù)量,也許明天產(chǎn)品經(jīng)理就讓你刪掉一個(gè)或者增加兩個(gè).那么這樣的維護(hù)方式顯然不能被接受.
更好的實(shí)現(xiàn)是有一個(gè)form.validata函數(shù), 它負(fù)責(zé)把真正的validata操作分發(fā)給每個(gè)組合對(duì)象.
form.validata函數(shù)里面會(huì)依次遍歷所有需要校驗(yàn)的field. 若有一個(gè)field校驗(yàn)未通過, form.validata都會(huì)返回false. 偽代碼如下.

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

十四 備忘錄模式

備忘錄模式在js中經(jīng)常用于數(shù)據(jù)緩存. 比如一個(gè)分頁控件, 從服務(wù)器獲得某一頁的數(shù)據(jù)后可以存入緩存。以后再翻回這一頁的時(shí)候,可以直接使用緩存里的數(shù)據(jù)而無需再次請(qǐng)求服務(wù)器。
實(shí)現(xiàn)比較簡(jiǎn)單,偽代碼:

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 );
})
}
}
}()

十五 職責(zé)鏈模式

職責(zé)鏈模式是一個(gè)對(duì)象A向另一個(gè)對(duì)象B發(fā)起請(qǐng)求,如果B不處理,可以把請(qǐng)求轉(zhuǎn)給C,如果C不處理,又可以把請(qǐng)求轉(zhuǎn)給D。一直到有一個(gè)對(duì)象愿意處理這個(gè)請(qǐng)求為止。

打個(gè)比方,客戶讓老板寫個(gè)php程序。老板肯定不寫,然后老板交給了部門經(jīng)理。部門經(jīng)理不愿意寫,又交給項(xiàng)目經(jīng)理。項(xiàng)目經(jīng)理不會(huì)寫,又交給程序員。最后由碼農(nóng)來完成。

在這個(gè)假設(shè)里, 有幾條職責(zé)鏈模式的特點(diǎn)。

1 老板只跟部門經(jīng)理打交道,部門經(jīng)理只聯(lián)系項(xiàng)目經(jīng)理,項(xiàng)目經(jīng)理只找碼農(nóng)的麻煩。
2 如果碼農(nóng)也不寫,這個(gè)項(xiàng)目將會(huì)流產(chǎn)。
3 客戶并不清楚這個(gè)程序最后是由誰寫出來的。
js中的事件冒泡就是作為一個(gè)職責(zé)鏈來實(shí)現(xiàn)的。一個(gè)事件在某個(gè)節(jié)點(diǎn)上被觸發(fā),然后向根節(jié)點(diǎn)傳遞, 直到被節(jié)點(diǎn)捕獲。

十六 享元模式

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

在webqq里面, 打開QQ好友列表往下拉的時(shí)候,會(huì)為每個(gè)好友創(chuàng)建一個(gè)div( 如果算上div中的子節(jié)點(diǎn), 還遠(yuǎn)不只1個(gè)元素 ).

如果有1000個(gè)QQ好友, 意味著如果從頭拉到尾, 會(huì)創(chuàng)建1000個(gè)div, 這時(shí)候有些瀏覽器也許已經(jīng)假死了. 這還只是一個(gè)隨便翻翻好友列表的操作.

所以我們想到了一種解決辦法, 當(dāng)滾動(dòng)條滾動(dòng)的時(shí)候, 把已經(jīng)消失在視線外的div都刪除掉. 這樣頁面可以保持只有一定數(shù)量的節(jié)點(diǎn). 問題是這樣頻繁的添加與刪除節(jié)點(diǎn), 也會(huì)造成很大的性能開銷, 而且這種感覺很不對(duì)味.

現(xiàn)在享元模式可以登場(chǎng)了. 顧名思義, 享元模式可以提供一些共享的對(duì)象以便重復(fù)利用. 仔細(xì)看下上圖, 其實(shí)我們一共只需要10個(gè)div來顯示好友信息,也就是出現(xiàn)在用戶視線中的10個(gè)div.這10個(gè)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();
}
}
/* 一個(gè)假設(shè)的事件,用來監(jiān)聽剛消失在視線外的div,實(shí)際上可以通過監(jiān)聽滾 動(dòng)條位置來實(shí)現(xiàn) */
userInfoContainer.disappear(function( div ){
created.push( div );
})
})()
var div = getDiv();
div.innerHTML = "${userinfo}";

原理其實(shí)很簡(jiǎn)單, 把剛隱藏起來的div放到一個(gè)數(shù)組中, 當(dāng)需要div的時(shí)候, 先從該數(shù)組中取, 如果數(shù)組中已經(jīng)沒有了, 再重新創(chuàng)建一個(gè). 這個(gè)數(shù)組里的div就是享元, 它們每一個(gè)都可以當(dāng)作任何用戶信息的載體.

當(dāng)然這只是個(gè)示例,實(shí)際的情況要復(fù)雜一些, 比如快速拖動(dòng)的時(shí)候, 我們可能還得為節(jié)點(diǎn)設(shè)置一個(gè)緩沖區(qū).

十七 狀態(tài)模式

狀態(tài)模式主要可以用于這種場(chǎng)景
1 一個(gè)對(duì)象的行為取決于它的狀態(tài)
2 一個(gè)操作中含有龐大的條件分支語句

回想下街頭霸王的游戲。

隆有走動(dòng),攻擊,防御,跌倒,跳躍等等多種狀態(tài),而這些狀態(tài)之間既有聯(lián)系又互相約束。比如跳躍的時(shí)候是不能攻擊和防御的。跌倒的時(shí)候既不能攻擊又不能防御,而走動(dòng)的時(shí)候既可以攻擊也可以跳躍。

要完成這樣一系列邏輯, 常理下if else是少不了的. 而且數(shù)量無法估計(jì), 特別是增加一種新狀態(tài)的時(shí)候, 可能要從代碼的第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, 并且方便修改和維護(hù), 我們引入一個(gè)狀態(tài)類.

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; 
//不成功,跳躍的時(shí)候不能防御
}
//do something; //防御的真正邏輯代碼, 為了防止?fàn)顟B(tài)類的代碼過多, 應(yīng)該把這些邏輯繼續(xù)扔給真正的fight類來執(zhí)行.
currState = 'defense'; 
// 切換狀態(tài)
}
}
var changeState = function( state ){
states[ state ] && states[ state ]();
}
return {
changeState : changeState
}
}
var stateManager = StateManager();
stateManager.changeState( 'defense' );

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

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

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

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

相關(guān)文章

最新評(píng)論