常用的Javascript設(shè)計(jì)模式小結(jié)
《Practical Common Lisp》的作者 Peter Seibel 曾說(shuō),如果你需要一種模式,那一定是哪里出了問(wèn)題。他所說(shuō)的問(wèn)題是指因?yàn)檎Z(yǔ)言的天生缺陷,不得不去尋求和總結(jié)一種通用的解決方案。
不管是弱類(lèi)型或強(qiáng)類(lèi)型,靜態(tài)或動(dòng)態(tài)語(yǔ)言,命令式或說(shuō)明式語(yǔ)言、每種語(yǔ)言都有天生的優(yōu)缺點(diǎn)。一個(gè)牙買(mǎi)加運(yùn)動(dòng)員, 在短跑甚至拳擊方面有一些優(yōu)勢(shì),在練瑜伽上就欠缺一些。
術(shù)士和暗影牧師很容易成為一個(gè)出色的輔助,而一個(gè)背著梅肯滿(mǎn)地圖飛的敵法就會(huì)略顯尷尬。 換到程序中, 靜態(tài)語(yǔ)言里可能需要花很多功夫來(lái)實(shí)現(xiàn)裝飾者,而js由于能隨時(shí)往對(duì)象上面扔方法,以至于裝飾者模式在js里成了雞肋。
講 Javascript 設(shè)計(jì)模式的書(shū)還比較少,《Pro javaScript Design Patterns》是比較經(jīng)典的一本,但是它里面的例子舉得比較啰嗦,所以結(jié)合我在工作中寫(xiě)過(guò)的代碼,把我的理解總結(jié)一下。如果我的理解出現(xiàn)了偏差,請(qǐng)不吝指正。
一 單例模式
單例模式的定義是產(chǎn)生一個(gè)類(lèi)的唯一實(shí)例,但js本身是一種“無(wú)類(lèi)”語(yǔ)言。很多講js設(shè)計(jì)模式的文章把{}當(dāng)成一個(gè)單例來(lái)使用也勉強(qiáng)說(shuō)得通。因?yàn)閖s生成對(duì)象的方式有很多種,我們來(lái)看下另一種更有意義的單例。
有這樣一個(gè)常見(jiàn)的需求,點(diǎn)擊某個(gè)按鈕的時(shí)候需要在頁(yè)面彈出一個(gè)遮罩層。比如web.qq.com點(diǎn)擊登錄的時(shí)候.
這個(gè)生成灰色背景遮罩層的代碼是很好寫(xiě)的.
var createMask = function(){ return document,body.appendChild( document.createElement(div) ); }
$( 'button' ).click( function(){ Var mask = createMask(); mask.show(); })
問(wèn)題是, 這個(gè)遮罩層是全局唯一的, 那么每次調(diào)用createMask都會(huì)創(chuàng)建一個(gè)新的div, 雖然可以在隱藏遮罩層的把它remove掉. 但顯然這樣做不合理.
再看下第二種方案, 在頁(yè)面的一開(kāi)始就創(chuàng)建好這個(gè)div. 然后用一個(gè)變量引用它.
var mask = document.body.appendChild( document.createElement( ''div' ) ); $( ''button' ).click( function(){ mask.show(); } )
這樣確實(shí)在頁(yè)面只會(huì)創(chuàng)建一個(gè)遮罩層div, 但是另外一個(gè)問(wèn)題隨之而來(lái), 也許我們永遠(yuǎn)都不需要這個(gè)遮罩層, 那又浪費(fèi)掉一個(gè)div, 對(duì)dom節(jié)點(diǎn)的任何操作都應(yīng)該非常吝嗇.
如果可以借助一個(gè)變量. 來(lái)判斷是否已經(jīng)創(chuàng)建過(guò)div呢?
var mask; var createMask = function(){ if ( mask ) return mask; else{ mask = document,body.appendChild( document.createElement(div) ); return mask; } }
看起來(lái)不錯(cuò), 到這里的確完成了一個(gè)產(chǎn)生單列對(duì)象的函數(shù). 我們?cè)僮屑?xì)看這段代碼有什么不妥.
首先這個(gè)函數(shù)是存在一定副作用的, 函數(shù)體內(nèi)改變了外界變量mask的引用, 在多人協(xié)作的項(xiàng)目中, createMask是個(gè)不安全的函數(shù). 另一方面, mask這個(gè)全局變量并不是非需不可. 再來(lái)改進(jìn)一下.
var createMask = function(){ var mask; return function(){ return mask || ( mask = document.body.appendChild( document.createElement('div') ) ) } }()
用了個(gè)簡(jiǎn)單的閉包把變量mask包起來(lái), 至少對(duì)于createMask函數(shù)來(lái)講, 它是封閉的.
可能看到這里, 會(huì)覺(jué)得單例模式也太簡(jiǎn)單了. 的確一些設(shè)計(jì)模式都是非常簡(jiǎn)單的, 即使從沒(méi)關(guān)注過(guò)設(shè)計(jì)模式的概念, 在平時(shí)的代碼中也不知不覺(jué)用到了一些設(shè)計(jì)模式. 就像多年前我明白老漢推車(chē)是什么回事的時(shí)候也想過(guò)尼瑪原來(lái)這就是老漢推車(chē).
GOF里的23種設(shè)計(jì)模式, 也是在軟件開(kāi)發(fā)中早就存在并反復(fù)使用的模式. 如果程序員沒(méi)有明確意識(shí)到他使用過(guò)某些模式, 那么下次他也許會(huì)錯(cuò)過(guò)更合適的設(shè)計(jì) (這段話(huà)來(lái)自《松本行弘的程序世界》).
再回來(lái)正題, 前面那個(gè)單例還是有缺點(diǎn). 它只能用于創(chuàng)建遮罩層. 假如我又需要寫(xiě)一個(gè)函數(shù), 用來(lái)創(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è)變量來(lái)保存第一次的返回值, 如果它已經(jīng)被賦值過(guò), 那么在以后的調(diào)用中優(yōu)先返回該變量. 而真正創(chuàng)建遮罩層的代碼是通過(guò)回調(diào)函數(shù)的方式傳人到singleton包裝器中的. 這種方式其實(shí)叫橋接模式. 關(guān)于橋接模式, 放在后面一點(diǎn)點(diǎn)來(lái)說(shuō).
然而singleton函數(shù)也不是完美的, 它始終還是需要一個(gè)變量result來(lái)寄存div的引用. 遺憾的是js的函數(shù)式特性還不足以完全的消除聲明和語(yǔ)句.
二 簡(jiǎn)單工廠(chǎng)模式
簡(jiǎn)單工廠(chǎng)模式是由一個(gè)方法來(lái)決定到底要?jiǎng)?chuàng)建哪個(gè)類(lèi)的實(shí)例, 而這些實(shí)例經(jīng)常都擁有相同的接口. 這種模式主要用在所實(shí)例化的類(lèi)型在編譯期并不能確定, 而是在執(zhí)行期決定的情況。 說(shuō)的通俗點(diǎn),就像公司茶水間的飲料機(jī),要咖啡還是牛奶取決于你按哪個(gè)按鈕。
簡(jiǎn)單工廠(chǎng)模式在創(chuàng)建ajax對(duì)象的時(shí)候也非常有用.
之前我寫(xiě)了一個(gè)處理ajax異步嵌套的庫(kù),地址在https://github.com/AlloyTeam/DanceRequest.
這個(gè)庫(kù)里提供了幾種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ǎng)方法, 至于到底是產(chǎn)生xhr的實(shí)例, 還是jsonp的實(shí)例. 是由后來(lái)的代碼決定的。
實(shí)際上在js里面,所謂的構(gòu)造函數(shù)也是一個(gè)簡(jiǎn)單工廠(chǎng)。只是批了一件new的衣服. 我們扒掉這件衣服看看里面。
通過(guò)這段代碼, 在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
這段代碼來(lái)自es5的new和構(gòu)造器的相關(guān)說(shuō)明, 可以看到,所謂的new, 本身只是一個(gè)對(duì)象的復(fù)制和改寫(xiě)過(guò)程, 而具體會(huì)生成什么是由調(diào)用ObjectFactory時(shí)傳進(jìn)去的參數(shù)所決定的。
三 觀(guān)察者模式
觀(guān)察者模式( 又叫發(fā)布者-訂閱者模式 )應(yīng)該是最常用的模式之一. 在很多語(yǔ)言里都得到大量應(yīng)用. 包括我們平時(shí)接觸的dom事件. 也是js和dom之間實(shí)現(xiàn)的一種觀(guān)察者模式.
div.onclick = function click (){ alert ( ''click' ) }
只要訂閱了div的click事件. 當(dāng)點(diǎn)擊div的時(shí)候, function click就會(huì)被觸發(fā).
那么到底什么是觀(guān)察者模式呢. 先看看生活中的觀(guān)察者模式。
好萊塢有句名言. “不要給我打電話(huà), 我會(huì)給你打電話(huà)”. 這句話(huà)就解釋了一個(gè)觀(guān)察者模式的來(lái)龍去脈。 其中“我”是發(fā)布者, “你”是訂閱者。
再舉個(gè)例子,我來(lái)公司面試的時(shí)候,完事之后每個(gè)面試官都會(huì)對(duì)我說(shuō):“請(qǐng)留下你的聯(lián)系方式, 有消息我們會(huì)通知你”。 在這里“我”是訂閱者, 面試官是發(fā)布者。所以我不用每天或者每小時(shí)都去詢(xún)問(wèn)面試結(jié)果, 通訊的主動(dòng)權(quán)掌握在了面試官手上。而我只需要提供一個(gè)聯(lián)系方式。
觀(guān)察者模式可以很好的實(shí)現(xiàn)2個(gè)模塊之間的解耦。 假如我正在一個(gè)團(tuán)隊(duì)里開(kāi)發(fā)一個(gè)html5游戲. 當(dāng)游戲開(kāi)始的時(shí)候,需要加載一些圖片素材。加載好這些圖片之后開(kāi)始才執(zhí)行游戲邏輯. 假設(shè)這是一個(gè)需要多人合作的項(xiàng)目. 我完成了Gamer和Map模塊, 而我的同事A寫(xiě)了一個(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(); } )
可是寫(xiě)這個(gè)模塊的同事A去了外地旅游. 于是我打電話(huà)給他, 喂. 你的loadImage函數(shù)在哪, 我能不能改一下, 改了之后有沒(méi)有副作用. 如你所想, 各種不淡定的事發(fā)生了. 如果當(dāng)初我們能這樣寫(xiě)呢:
loadImage.listen( ''ready', function(){ Map.init(); }) loadImage.listen( ''ready', function(){ Gamer.init(); }) loadImage.listen( ''ready', function(){ Sount.init(); })
loadImage完成之后, 它根本不關(guān)心將來(lái)會(huì)發(fā)生什么, 因?yàn)樗墓ぷ饕呀?jīng)完成了. 接下來(lái)它只要發(fā)布一個(gè)信號(hào).
loadImage.trigger( ”ready' );
那么監(jiān)聽(tīng)了loadImage的'ready'事件的對(duì)象都會(huì)收到通知. 就像上個(gè)面試的例子. 面試官根本不關(guān)心面試者們收到面試結(jié)果后會(huì)去哪吃飯. 他只負(fù)責(zé)把面試者的簡(jiǎn)歷搜集到一起. 當(dāng)面試結(jié)果出來(lái)時(shí)照著簡(jiǎn)歷上的電話(huà)挨個(gè)通知.
說(shuō)了這么多概念, 來(lái)一個(gè)具體的實(shí)現(xiàn). 實(shí)現(xiàn)過(guò)程其實(shí)很簡(jiǎn)單. 面試者把簡(jiǎn)歷扔到一個(gè)盒子里, 然后面試官在合適的時(shí)機(jī)拿著盒子里的簡(jiǎn)歷挨個(gè)打電話(huà)通知結(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() { //面試官打電話(huà)通知面試者 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 } }
最后用觀(guān)察者模式來(lái)做一個(gè)成人電視臺(tái)的小應(yīng)用.
//訂閱者 var adultTv = Event(); adultTv .listen( ''play', function( data ){ alert ( "今天是誰(shuí)的電影" + data.name ); }); //發(fā)布者 adultTv .trigger( ''play', { 'name': '麻生希' } )
四 適配器模式
去年年前當(dāng)時(shí)正在開(kāi)發(fā)dev.qplus.com, 有個(gè)存儲(chǔ)應(yīng)用分類(lèi)id的js文件, 分類(lèi)id的結(jié)構(gòu)最開(kāi)始設(shè)計(jì)的比較笨重. 于是我決定重構(gòu)它. 我把它定義成一個(gè)json樹(shù)的形式, 大概是這樣:
var category = { music: { id: 1, children: [ , , , , ] } }
dev.qplus.com里大概有4,5個(gè)頁(yè)面都調(diào)用這個(gè)category對(duì)象. 春節(jié)前我休了1個(gè)星期假. 過(guò)年來(lái)之后發(fā)現(xiàn)郵箱里有封郵件, 設(shè)計(jì)數(shù)據(jù)庫(kù)的同學(xué)把category..js也重構(gòu)了一份, 并且其他幾個(gè)項(xiàng)目里都是用了這份category.js, 我拿過(guò)來(lái)一看就傻眼了, 和我之前定的數(shù)據(jù)結(jié)構(gòu)完全不一樣.
當(dāng)然這是一個(gè)溝通上的反面例子. 但接下來(lái)的重點(diǎn)是我已經(jīng)在N個(gè)文件里用到了之前我定的category.js. 而且惹上了一些復(fù)雜的相關(guān)邏輯. 怎么改掉我之前的代碼呢. 全部重寫(xiě)肯定是不愿意. 所以現(xiàn)在適配器就派上用場(chǎng)了.
只需要把同事的category用一個(gè)函數(shù)轉(zhuǎn)成跟我之前定義的一樣.
my.category = adapterCategory ( afu.category );
適配器模式的作用很像一個(gè)轉(zhuǎn)接口. 本來(lái)iphone的充電器是不能直接插在電腦機(jī)箱上的, 而通過(guò)一個(gè)usb轉(zhuǎn)接口就可以了.
所以, 在程序里適配器模式也經(jīng)常用來(lái)適配2個(gè)接口, 比如你現(xiàn)在正在用一個(gè)自定義的js庫(kù). 里面有個(gè)根據(jù)id獲取節(jié)點(diǎn)的方法$id(). 有天你覺(jué)得jquery里的$實(shí)現(xiàn)得更酷, 但你又不想讓你的工程師去學(xué)習(xí)新的庫(kù)和語(yǔ)法. 那一個(gè)適配器就能讓你完成這件事情.
$id = function( id ){ return jQuery( '#' + id )[0]; }
五 代理模式
代理模式的定義是把對(duì)一個(gè)對(duì)象的訪(fǎng)問(wèn), 交給另一個(gè)代理對(duì)象來(lái)操作.
舉一個(gè)例子, 我在追一個(gè)MM想給她送一束花,但是我因?yàn)槲倚愿癖容^靦腆,所以我托付了MM的一個(gè)好朋友來(lái)送。
這個(gè)例子不是非常好, 至少我們沒(méi)看出代理模式有什么大的用處,因?yàn)樽稭M更好的方式是送一臺(tái)寶馬。
再舉個(gè)例子,假如我每天都得寫(xiě)工作日?qǐng)?bào)( 其實(shí)沒(méi)有這么慘 ). 我的日?qǐng)?bào)最后會(huì)讓總監(jiān)審閱. 如果我們都直接把日?qǐng)?bào)發(fā)給 總監(jiān) , 那可能 總監(jiān) 就沒(méi)法工作了. 所以通常的做法是把日?qǐng)?bào)發(fā)給我的組長(zhǎng) , 組長(zhǎng)把所有組員一周的日?qǐng)?bào)都匯總后再發(fā)給總監(jiān) .
實(shí)際的編程中, 這種因?yàn)樾阅軉?wèn)題使用代理模式的機(jī)會(huì)是非常多的。比如頻繁的訪(fǎng)問(wèn)dom節(jié)點(diǎn), 頻繁的請(qǐng)求遠(yuǎn)程資源. 可以把操作先存到一個(gè)緩沖區(qū), 然后自己選擇真正的觸發(fā)時(shí)機(jī).
再來(lái)個(gè)詳細(xì)的例子,之前我寫(xiě)了一個(gè)街頭霸王的游戲, 地址在http://alloyteam.github.com/StreetFighter/
游戲中隆需要接受鍵盤(pán)的事件, 來(lái)完成相應(yīng)動(dòng)作.
于是我寫(xiě)了一個(gè)keyManage類(lèi). 其中在游戲主線(xiàn)程里監(jiān)聽(tīng)keyManage的變化.
var keyMgr = keyManage(); keyMgr.listen( ''change', function( keyCode ){ console.log( keyCode ); });
圖片里面隆正在放升龍拳, 升龍拳的操作是前下前+拳. 但是這個(gè)keyManage類(lèi)只要發(fā)生鍵盤(pán)事件就會(huì)觸發(fā)之前監(jiān)聽(tīng)的change函數(shù). 這意味著永遠(yuǎn)只能取得前,后,前,拳這樣單獨(dú)的按鍵事件,而無(wú)法得到一個(gè)按鍵組合。
好吧,我決定改寫(xiě)我的keyManage類(lèi), 讓它也支持傳遞按鍵組合. 但是如果我以后寫(xiě)個(gè)html5版雙截龍,意味著我每次都得改寫(xiě)keyManage. 我總是覺(jué)得, 這種函數(shù)應(yīng)該可以抽象成一個(gè)更底層的方法, 讓任何游戲都可以用上它.
所以最后的keyManage只負(fù)責(zé)映射鍵盤(pán)事件. 而隆接受到的動(dòng)作是通過(guò)一個(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í)候,無(wú)論是各種開(kāi)源庫(kù),還是自己寫(xiě)的Ajax類(lèi), 都會(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)部分和抽象部分分離開(kāi)來(lái), 以便兩者可以獨(dú)立的變化。在實(shí)現(xiàn)api的時(shí)候, 橋接模式特別有用。比如最開(kāi)始的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ú)自變化互不影響。 如果需要再寫(xiě)一個(gè)單例的createScript就一點(diǎn)也不費(fèi)力.
var createScript = singleton( function(){ return document.body.appendChild( document.createElement('script') ); })
另外一個(gè)常見(jiàn)的例子就是forEach函數(shù)的實(shí)現(xiàn), 用來(lái)迭代一個(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ù)的改寫(xiě)影響.
forEach( [1,2,3], function( i, n ){ alert ( n*2 ) } ) forEach( [1,2,3], function( i, n ){ alert ( n*3 ) } )
七 外觀(guān)模式
外觀(guān)模式(門(mén)面模式),是一種相對(duì)簡(jiǎn)單而又無(wú)處不在的模式。外觀(guān)模式提供一個(gè)高層接口,這個(gè)接口使得客戶(hù)端或子系統(tǒng)更加方便調(diào)用。
用一段再簡(jiǎn)單不過(guò)的代碼來(lái)表示
var getName = function(){ return ''svenzeng" } var getSex = function(){ return 'man' }
如果你需要分別調(diào)用getName和getSex函數(shù). 那可以用一個(gè)更高層的接口getUserInfo來(lái)調(diào)用.
var getUserInfo = function(){ var info = a() + b(); return info; }
也許你會(huì)問(wèn)為什么一開(kāi)始不把getName和getSex的代碼寫(xiě)到一起, 比如這樣
var getNameAndSex = function(){ return 'svenzeng" + "man"; }
答案是顯而易見(jiàn)的,飯?zhí)玫某床藥煾挡粫?huì)因?yàn)槟泐A(yù)定了一份燒鴨和一份白菜就把這兩樣菜炒在一個(gè)鍋里。他更愿意給你提供一個(gè)燒鴨飯?zhí)撞汀M瑯釉诔绦蛟O(shè)計(jì)中,我們需要保證函數(shù)或者對(duì)象盡可能的處在一個(gè)合理粒度,畢竟不是每個(gè)人喜歡吃燒鴨的同時(shí)又剛好喜歡吃白菜。
外觀(guān)模式還有一個(gè)好處是可以對(duì)用戶(hù)隱藏真正的實(shí)現(xiàn)細(xì)節(jié),用戶(hù)只關(guān)心最高層的接口。比如在燒鴨飯?zhí)撞偷墓适轮校悴⒉魂P(guān)心師傅是先做燒鴨還是先炒白菜,你也不關(guān)心那只鴨子是在哪里成長(zhǎng)的。
最后寫(xiě)個(gè)我們都用過(guò)的外觀(guān)模式例子
var stopEvent = function( e ){ //同時(shí)阻止事件默認(rèn)行為和冒泡 e.stopPropagation(); e.preventDefault(); }
八 訪(fǎng)問(wèn)者模式
GOF官方定義: 訪(fǎng)問(wèn)者模式是表示一個(gè)作用于某個(gè)對(duì)象結(jié)構(gòu)中的各元素的操作。它使可以在不改變各元素的類(lèi)的前提下定義作用于這些元素的新操作。我們?cè)谑褂靡恍┎僮鲗?duì)不同的對(duì)象進(jìn)行處理時(shí),往往會(huì)根據(jù)不同的對(duì)象選擇不同的處理方法和過(guò)程。在實(shí)際的代碼過(guò)程中,我們可以發(fā)現(xiàn),如果讓所有的操作分散到各個(gè)對(duì)象中,整個(gè)系統(tǒng)會(huì)變得難以維護(hù)和修改。且增加新的操作通常都要重新編譯所有的類(lèi)。因此,為了解決這個(gè)問(wèn)題,我們可以將每一個(gè)類(lèi)中的相關(guān)操作提取出來(lái),包裝成一個(gè)獨(dú)立的對(duì)象,這個(gè)對(duì)象我們就稱(chēng)為訪(fǎng)問(wèn)者(Visitor)。利用訪(fǎng)問(wèn)者,對(duì)訪(fǎng)問(wèn)的元素進(jìn)行某些操作時(shí),只需將此對(duì)象作為參數(shù)傳遞給當(dāng)前訪(fǎng)問(wèn)者,然后,訪(fǎng)問(wèn)者會(huì)依據(jù)被訪(fǎng)問(wèn)者的具體信息,進(jìn)行相關(guān)的操作。
據(jù)統(tǒng)計(jì),上面這段話(huà)只有5%的人會(huì)看到最后一句。那么通俗點(diǎn)講,訪(fǎng)問(wèn)者模式先把一些可復(fù)用的行為抽象到一個(gè)函數(shù)(對(duì)象)里,這個(gè)函數(shù)我們就稱(chēng)為訪(fǎng)問(wèn)者(Visitor)。如果另外一些對(duì)象要調(diào)用這個(gè)函數(shù),只需要把那些對(duì)象當(dāng)作參數(shù)傳給這個(gè)函數(shù),在js里我們經(jīng)常通過(guò)call或者apply的方式傳遞this對(duì)象給一個(gè)Visitor函數(shù).
訪(fǎng)問(wèn)者模式也被稱(chēng)為GOF總結(jié)的23種設(shè)計(jì)模式中最難理解的一種。不過(guò)這有很大一部分原因是因?yàn)椤对O(shè)計(jì)模式》基于C++和Smalltalk寫(xiě)成. 在強(qiáng)類(lèi)型語(yǔ)言中需要通過(guò)多次重載來(lái)實(shí)現(xiàn)訪(fǎng)問(wèn)者的接口匹配。
而在js這種基于鴨子類(lèi)型的語(yǔ)言中,訪(fǎng)問(wèn)者模式幾乎是原生的實(shí)現(xiàn), 所以我們可以利用apply和call毫不費(fèi)力的使用訪(fǎng)問(wèn)者模式,這一小節(jié)更關(guān)心的是這種模式的思想以及在js引擎中的實(shí)現(xiàn)。
我們先來(lái)了解一下什么是鴨子類(lèi)型,說(shuō)個(gè)故事:
很久以前有個(gè)皇帝喜歡聽(tīng)鴨子呱呱叫,于是他召集大臣組建一個(gè)一千只鴨子的合唱團(tuán)。大臣把全國(guó)的鴨子都抓來(lái)了,最后始終還差一只。有天終于來(lái)了一只自告奮勇的雞,這只雞說(shuō)它也會(huì)呱呱叫,好吧在這個(gè)故事的設(shè)定里,它確實(shí)會(huì)呱呱叫。 后來(lái)故事的發(fā)展很明顯,這只雞混到了鴨子的合唱團(tuán)中。— 皇帝只是想聽(tīng)呱呱叫,他才不在乎你是鴨子還是雞呢。
這個(gè)就是鴨子類(lèi)型的概念,在js這種弱類(lèi)型語(yǔ)言里,很多方法里都不做對(duì)象的類(lèi)型檢測(cè),而是只關(guān)心這些對(duì)象能做什么。
Array構(gòu)造器和String構(gòu)造器的prototype上的方法就被特意設(shè)計(jì)成了訪(fǎng)問(wèn)者。這些方法不對(duì)this的數(shù)據(jù)類(lèi)型做任何校驗(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方法沒(méi)有對(duì)this的類(lèi)型做任何顯示的限制,所以理論上任何對(duì)象都可以被傳入ArrayPush這個(gè)訪(fǎng)問(wèn)者。
不過(guò)在代碼的執(zhí)行期,還是會(huì)受到一些隱式限制,在上面的例子很容易看出要求:
1、 this對(duì)象上面可儲(chǔ)存屬性. //反例: 值類(lèi)型的數(shù)據(jù)
2、 this的length屬性可寫(xiě). //反例: functon對(duì)象, function有一個(gè)只讀的length屬性, 表示形參個(gè)數(shù).
如果不符合這2條規(guī)則的話(huà),代碼在執(zhí)行期會(huì)報(bào)錯(cuò). 也就是說(shuō), Array.prototype.push.call( 1, ‘first' )和Array.prototoype.push.call( function(){}, ‘first' )都達(dá)不到預(yù)期的效果.
利用訪(fǎng)問(wèn)者,我們來(lái)做個(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è)封裝起來(lái),并且使它們可相互替換。
一個(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))就是一種策略模式的封裝.
再來(lái)一個(gè)例子. 上半年我寫(xiě)的dev.qplus.com, 很多頁(yè)面都會(huì)有個(gè)即時(shí)驗(yàn)證的表單. 表單的每個(gè)成員都會(huì)有一些不同的驗(yàn)證規(guī)則.
比如姓名框里面, 需要驗(yàn)證非空,敏感詞,字符過(guò)長(zhǎng)這幾種情況。 當(dāng)然是可以寫(xiě)3個(gè)if else來(lái)解決,不過(guò)這樣寫(xiě)代碼的擴(kuò)展性和維護(hù)性可想而知。如果表單里面的元素多一點(diǎn),需要校驗(yàn)的情況多一點(diǎn),加起來(lái)寫(xiě)上百個(gè)if else也不是沒(méi)有可能。
所以更好的做法是把每種驗(yàn)證規(guī)則都用策略模式單獨(dú)的封裝起來(lái)。需要哪種驗(yàn)證的時(shí)候只需要提供這個(gè)策略的名字。就像這樣:
nameInput.addValidata({ notNull: true, dirtyWords: true, maxLength: 30 }) 而notNull,maxLength等方法只需要統(tǒng)一的返回true或者false,來(lái)表示是否通過(guò)了驗(yàn)證。 validataList = { notNull: function( value ){ return value !== ''; }, maxLength: function( value, maxLen ){ return value.length() > maxLen; } }
可以看到,各種驗(yàn)證規(guī)則很容易被修改和相互替換。如果某天產(chǎn)品經(jīng)理建議字符過(guò)長(zhǎng)的限制改成60個(gè)字符。那只需要0.5秒完成這次工作。
十 模版方法模式
模式方法是預(yù)先定義一組算法,先把算法的不變部分抽象到父類(lèi),再將另外一些可變的步驟延遲到子類(lèi)去實(shí)現(xiàn)。聽(tīng)起來(lái)有點(diǎn)像工廠(chǎng)模式( 非前面說(shuō)過(guò)的簡(jiǎn)單工廠(chǎng)模式 ).
最大的區(qū)別是,工廠(chǎng)模式的意圖是根據(jù)子類(lèi)的實(shí)現(xiàn)最終獲得一種對(duì)象. 而模版方法模式著重于父類(lèi)對(duì)子類(lèi)的控制.
按GOF的描敘,模版方法導(dǎo)致一種反向的控制結(jié)構(gòu),這種結(jié)構(gòu)有時(shí)被稱(chēng)為“好萊塢法則”,即“別找我們,我們找你”。這指的是一個(gè)父類(lèi)調(diào)用一個(gè)子類(lèi)的操作,而不是相反。
一個(gè)很常用的場(chǎng)景是在一個(gè)公司的項(xiàng)目中,經(jīng)常由架構(gòu)師搭好架構(gòu),聲明出抽象方法。下面的程序員再去分頭重寫(xiě)這些抽象方法。
在深入了解之前,容許我先扯遠(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ù)先定義的算法中不變部分. 所有子類(lèi)都不能改寫(xiě)它. 如果需要我們可以寫(xiě)成protected的類(lèi)型.
而其他的函數(shù)在父類(lèi)中會(huì)被先定義成一個(gè)空函數(shù)(鉤子). 然后被子類(lèi)重寫(xiě),這就是模版方法中所謂的可變的步驟。
假設(shè)有個(gè)子類(lèi)哺乳動(dòng)物類(lèi)繼承了Life類(lèi).
var Mammal = function(){ } Mammal.prototype = Life.prototype; //繼承Life
然后重寫(xiě)出生和衰老這兩個(gè)鉤子函數(shù).
Mammal.prototope.出生 = function(){ '胎生() } Mammal.prototype.成長(zhǎng) = function(){ //再留給子類(lèi)去實(shí)現(xiàn) } Mammal.prototope.衰老 = function(){ 自由基的過(guò)氧化反應(yīng)() } Life.prototype.死亡 = function(){ //再留給子類(lèi)去實(shí)現(xiàn) } //再實(shí)現(xiàn)一個(gè)Dog類(lèi) var = Dog = function(){ } //Dog繼承自哺乳動(dòng)物. Dog.prototype = Mammal.prototype; var dog = new Dog(); dog.init();
至此,一只小狗的生命會(huì)依次經(jīng)歷DNA復(fù)制,出生,成長(zhǎng),衰老,死亡這幾個(gè)過(guò)程。這些步驟早在它出生前就決定了。所幸的是,上帝沒(méi)有安排好它生命的所有細(xì)節(jié)。它還是能通過(guò)對(duì)成長(zhǎng)函數(shù)的重寫(xiě),來(lái)成為一只與眾不同的小狗。
舉個(gè)稍微現(xiàn)實(shí)點(diǎn)的例子,游戲大廳中的所有游戲都有登錄,游戲中,游戲結(jié)束這幾個(gè)過(guò)程,而登錄和游戲結(jié)束之后彈出提示這些函數(shù)都是應(yīng)該公用的。
那么首先需要的是一個(gè)父類(lèi)。
var gameCenter = function(){ } gameCenter.ptototype.init = function(){ this.login(); this.gameStart(); this.end(); } gameCenter.prototype.login= function(){ //do something } gameCenter.prototype.gameStart= function(){ //空函數(shù), 留給子類(lèi)去重寫(xiě) } gameCenter.prototype.end= function(){ alert ( "歡迎下次再來(lái)玩" ); }
接下來(lái)創(chuàng)建一個(gè)斗地主的新游戲, 只需要繼承g(shù)ameCenter然后重寫(xiě)它的gameStart函數(shù).
var 斗地主 = function(){ } 斗地主.prototype = gameCenter.prototype; //繼承 斗地主.prototype.gameStart = function(){ //do something } (new 斗地主).init();
這樣一局新的游戲就開(kāi)始了.
十一 中介者模式
中介者對(duì)象可以讓各個(gè)對(duì)象之間不需要顯示的相互引用,從而使其耦合松散,而且可以獨(dú)立的改變它們之間的交互。
打個(gè)比方,軍火買(mǎi)賣(mài)雙方為了安全起見(jiàn),找了一個(gè)信任的中介來(lái)進(jìn)行交易。買(mǎi)家A把錢(qián)交給中介B,然后從中介手中得到軍火,賣(mài)家C把軍火賣(mài)給中介,然后從中介手中拿回錢(qián)。一場(chǎng)交易完畢,A甚至不知道C是一只猴子還是一只猛犸。因?yàn)橹薪榈拇嬖?,A也未必一定要買(mǎi)C的軍火,也可能是D,E,F(xiàn)。
銀行在存款人和貸款人之間也能看成一個(gè)中介。存款人A并不關(guān)心他的錢(qián)最后被誰(shuí)借走。貸款人B也不關(guān)心他借來(lái)的錢(qián)來(lái)自誰(shuí)的存款。因?yàn)橛兄薪榈拇嬖冢@場(chǎng)交易才變得如此方便。
中介者模式和代理模式有一點(diǎn)點(diǎn)相似。都是第三者對(duì)象來(lái)連接2個(gè)對(duì)象的通信。具體差別可以從下圖中區(qū)別。
代理模式:
中介者模式
代理模式中A必然是知道B的一切,而中介者模式中A,B,C對(duì)E,F,G的實(shí)現(xiàn)并不關(guān)心.而且中介者模式可以連接任意多種對(duì)象。
切回到程序世界里的mvc,無(wú)論是j2ee中struts的Action. 還是js中backbone.js和spine.js里的Controler. 都起到了一個(gè)中介者的作用.
拿backbone舉例. 一個(gè)mode里的數(shù)據(jù)并不確定最后被哪些view使用. view需要的數(shù)據(jù)也可以來(lái)自任意一個(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' ); } ) })
十二 迭代器模式
迭代器模式提供一種方法順序訪(fǎng)問(wèn)一個(gè)聚合對(duì)象中各個(gè)元素,而又不需要暴露該方法中的內(nèi)部表示。
js中我們經(jīng)常會(huì)封裝一個(gè)each函數(shù)用來(lái)實(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ì)象組合成樹(shù)形結(jié)構(gòu)。使得用戶(hù)只需要操作最上層的接口,就可以對(duì)所有成員做相同的操作。
一個(gè)再好不過(guò)的例子就是jquery對(duì)象,大家都知道1個(gè)jquery對(duì)象其實(shí)是一組對(duì)象集合。比如在這樣一個(gè)HTML頁(yè)面
<div> <span></span> <span></span> </div>
我們想取消所有節(jié)點(diǎn)上綁定的事件, 需要這樣寫(xiě)
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方法.
再來(lái)個(gè)具體點(diǎn)的例子, 還是dev.qplus.com這個(gè)網(wǎng)站的即時(shí)驗(yàn)證表單。
注意下面那個(gè)修改資料的按鈕,如果有任意一個(gè)field的驗(yàn)證沒(méi)有通過(guò),修改資料的按鈕都將是灰色不可點(diǎn)的狀態(tài)。 這意味著我們重新填寫(xiě)了表單內(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è)外觀(guān)模式也能勉強(qiáng)解決這里條件分支堆砌的問(wèn)題,但真正的問(wèn)題是,我們并不能保證表單里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)未通過(guò), 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è)分頁(yè)控件, 從服務(wù)器獲得某一頁(yè)的數(shù)據(jù)后可以存入緩存。以后再翻回這一頁(yè)的時(shí)候,可以直接使用緩存里的數(shù)據(jù)而無(wú)需再次請(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è)比方,客戶(hù)讓老板寫(xiě)個(gè)php程序。老板肯定不寫(xiě),然后老板交給了部門(mén)經(jīng)理。部門(mén)經(jīng)理不愿意寫(xiě),又交給項(xiàng)目經(jīng)理。項(xiàng)目經(jīng)理不會(huì)寫(xiě),又交給程序員。最后由碼農(nóng)來(lái)完成。
在這個(gè)假設(shè)里, 有幾條職責(zé)鏈模式的特點(diǎn)。
1 老板只跟部門(mén)經(jīng)理打交道,部門(mén)經(jīng)理只聯(lián)系項(xiàng)目經(jīng)理,項(xiàng)目經(jīng)理只找碼農(nóng)的麻煩。
2 如果碼農(nóng)也不寫(xiě),這個(gè)項(xiàng)目將會(huì)流產(chǎn)。
3 客戶(hù)并不清楚這個(gè)程序最后是由誰(shuí)寫(xiě)出來(lái)的。
js中的事件冒泡就是作為一個(gè)職責(zé)鏈來(lái)實(shí)現(xiàn)的。一個(gè)事件在某個(gè)節(jié)點(diǎn)上被觸發(fā),然后向根節(jié)點(diǎn)傳遞, 直到被節(jié)點(diǎn)捕獲。
十六 享元模式
享元模式主要用來(lái)減少程序所需的對(duì)象個(gè)數(shù). 有一個(gè)例子, 我們這邊的前端同學(xué)幾乎人手一本《javascript權(quán)威指南》. 從省錢(qián)的角度講, 大約三本就夠了. 放在部門(mén)的書(shū)柜里, 誰(shuí)需要看的時(shí)候就去拿, 看完了還回去. 如果同時(shí)有4個(gè)同學(xué)需要看, 此時(shí)再去多買(mǎi)一本.
在webqq里面, 打開(kāi)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)消失在視線(xiàn)外的div都刪除掉. 這樣頁(yè)面可以保持只有一定數(shù)量的節(jié)點(diǎn). 問(wèn)題是這樣頻繁的添加與刪除節(jié)點(diǎn), 也會(huì)造成很大的性能開(kāi)銷(xiāo), 而且這種感覺(jué)很不對(duì)味.
現(xiàn)在享元模式可以登場(chǎng)了. 顧名思義, 享元模式可以提供一些共享的對(duì)象以便重復(fù)利用. 仔細(xì)看下上圖, 其實(shí)我們一共只需要10個(gè)div來(lái)顯示好友信息,也就是出現(xiàn)在用戶(hù)視線(xiàn)中的10個(gè)div.這10個(gè)div就可以寫(xiě)成享元.
偽代碼如下.
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è)的事件,用來(lái)監(jiān)聽(tīng)剛消失在視線(xiàn)外的div,實(shí)際上可以通過(guò)監(jiān)聽(tīng)滾 動(dòng)條位置來(lái)實(shí)現(xiàn) */ userInfoContainer.disappear(function( div ){ created.push( div ); }) })() var div = getDiv(); div.innerHTML = "${userinfo}";
原理其實(shí)很簡(jiǎn)單, 把剛隱藏起來(lái)的div放到一個(gè)數(shù)組中, 當(dāng)需要div的時(shí)候, 先從該數(shù)組中取, 如果數(shù)組中已經(jīng)沒(méi)有了, 再重新創(chuàng)建一個(gè). 這個(gè)數(shù)組里的div就是享元, 它們每一個(gè)都可以當(dāng)作任何用戶(hù)信息的載體.
當(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è)操作中含有龐大的條件分支語(yǔ)句
回想下街頭霸王的游戲。
隆有走動(dòng),攻擊,防御,跌倒,跳躍等等多種狀態(tài),而這些狀態(tài)之間既有聯(lián)系又互相約束。比如跳躍的時(shí)候是不能攻擊和防御的。跌倒的時(shí)候既不能攻擊又不能防御,而走動(dòng)的時(shí)候既可以攻擊也可以跳躍。
要完成這樣一系列邏輯, 常理下if else是少不了的. 而且數(shù)量無(wú)法估計(jì), 特別是增加一種新?tīng)顟B(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)類(lè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)類(lèi)的代碼過(guò)多, 應(yīng)該把這些邏輯繼續(xù)扔給真正的fight類(lèi)來(lái)執(zhí)行. currState = 'defense'; // 切換狀態(tài) } } var changeState = function( state ){ states[ state ] && states[ state ](); } return { changeState : changeState } } var stateManager = StateManager(); stateManager.changeState( 'defense' );
通過(guò)這個(gè)狀態(tài)類(lèi),可以把散落在世界各地的條件分支集中管理到一個(gè)類(lèi)里,并且可以很容易的添加一種新的狀態(tài)。而作為調(diào)用者,只需要通過(guò)暴露的changeState接口來(lái)切換人物的狀態(tài)。
/***************************分界線(xiàn)1******************************************/
GOF提出的23種設(shè)計(jì)模式,至此已經(jīng)寫(xiě)完大半。還有一些要么是js里不太適用,要么是js中已有原生自帶的實(shí)現(xiàn),所以就沒(méi)再去深究。這2篇文章里的大部分例子都來(lái)自或改寫(xiě)自工作和學(xué)習(xí)中的代碼。我對(duì)設(shè)計(jì)模式的看法是不用刻意去學(xué)習(xí)設(shè)計(jì)模式,平時(shí)我們接觸的很多代碼里已經(jīng)包含了一些設(shè)計(jì)模式的實(shí)現(xiàn)。我的過(guò)程是讀過(guò)prototype和jquery的源碼后,回頭翻設(shè)計(jì)模式的書(shū),發(fā)現(xiàn)不知覺(jué)中已經(jīng)接觸過(guò)十之六七。
同樣在實(shí)際的編碼中也沒(méi)有必要刻意去使用一些設(shè)計(jì)模式。就如同tokyo hot 32式一樣,在一場(chǎng)友好的papapa過(guò)程中,沒(méi)有必要去刻意使用某種姿勢(shì)。一切還是看需求和感覺(jué)。
- JavaScript設(shè)計(jì)模式--簡(jiǎn)單工廠(chǎng)模式定義與應(yīng)用案例詳解
- javascript設(shè)計(jì)模式 – 簡(jiǎn)單工廠(chǎng)模式原理與應(yīng)用實(shí)例分析
- Javascript設(shè)計(jì)模式理論與編程實(shí)戰(zhàn)之簡(jiǎn)單工廠(chǎng)模式
- JS面向?qū)ο蠡A(chǔ)講解(工廠(chǎng)模式、構(gòu)造函數(shù)模式、原型模式、混合模式、動(dòng)態(tài)原型模式)
- js面向?qū)ο笾R?jiàn)創(chuàng)建對(duì)象的幾種方式(工廠(chǎng)模式、構(gòu)造函數(shù)模式、原型模式)
- JavaScript 模式之工廠(chǎng)模式(Factory)應(yīng)用介紹
- javascript 模式設(shè)計(jì)之工廠(chǎng)模式學(xué)習(xí)心得
- JavaScript創(chuàng)建對(duì)象方式總結(jié)【工廠(chǎng)模式、構(gòu)造函數(shù)模式、原型模式等】
- JavaScript設(shè)計(jì)模式之觀(guān)察者模式(發(fā)布者-訂閱者模式)
- 常用的javascript設(shè)計(jì)模式
- JavaScript設(shè)計(jì)模式--簡(jiǎn)單工廠(chǎng)模式實(shí)例分析【XHR工廠(chǎng)案例】
相關(guān)文章
JavaScript獲取GridView中用戶(hù)點(diǎn)擊控件的行號(hào),列號(hào)
GridView中的某幾列有按鈕,需要獲取用戶(hù)當(dāng)前點(diǎn)的按鈕的行號(hào)(捎帶的得到列號(hào))2009-04-04深入理解javascript prototype的相關(guān)知識(shí)
這篇文章主要介紹了深入理解javascript prototype的相關(guān)知識(shí),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-09-09JS+CSS實(shí)現(xiàn)網(wǎng)頁(yè)加載中的動(dòng)畫(huà)效果
這篇文章主要為大家詳細(xì)介紹了JS+CSS實(shí)現(xiàn)網(wǎng)頁(yè)加載中的動(dòng)畫(huà)效果,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-10-10BootStrap中Tab頁(yè)簽切換實(shí)例代碼
這篇文章主要介紹了BootStrap中Tab頁(yè)簽切換實(shí)例代碼的相關(guān)資料,非常不錯(cuò)具有參考借鑒價(jià)值,需要的朋友可以參考下2016-05-05關(guān)于uniapp中onReachBottomDistance屬性的使用
這篇文章主要介紹了關(guān)于uniapp中onReachBottomDistance屬性的使用方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-09-09JavaScript常用基礎(chǔ)知識(shí)強(qiáng)化學(xué)習(xí)
這篇文章主要介紹了JavaScript常用基礎(chǔ)知識(shí)強(qiáng)化學(xué)習(xí),需要的朋友可以參考下2015-12-12