常用的Javascript設計模式

<div class="copyright-area">原文出處: <a target="_blank" href="http://www.alloyteam.com/2012/10/common-javascript-design-patterns/">alloyteam 博客-svenzeng</a>&nbsp;&nbsp;&nbsp;</div>


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

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

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

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

一 單例模式

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

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

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

JavaScript

JavaScript

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

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

JavaScript

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

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

JavaScript

看起來不錯, 到這裏的確完成了一個產生單列對象的函數. 我們再仔細看這段代碼有什麼不妥.

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

JavaScript

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

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

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

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

js中函數是第一型, 意味着函數也可以當參數傳遞. 看看最終的代碼.

JavaScript

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

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

二 簡單工廠模式

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

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

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

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

JavaScript

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

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

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

JavaScript

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

三 觀察者模式

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

JavaScript

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

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

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

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

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

loadImage的代碼如下

JavaScript

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

JavaScript

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

JavaScript

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

loadImage.trigger( ”ready’ );

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

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

JavaScript

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

//訂閱者

JavaScript

四 適配器模式

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

JavaScript

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

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

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

JavaScript

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

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

JavaScript

五 代理模式

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

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

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

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

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

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

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

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

JavaScript

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

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

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

JavaScript

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

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

JavaScript

六 橋接模式

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

JavaScript

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

JavaScript

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

JavaScript

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

JavaScript

七 外觀模式

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

JavaScript

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

JavaScript

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

JavaScript

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

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

JavaScript

八 訪問者模式

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的代碼:

JavaScript

可以看到,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方法.

JavaScript

九 策略模式

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

JavaScript

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

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

JavaScript

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

十 模版方法模式

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

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

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

JavaScript

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

JavaScript

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

JavaScript

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

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

JavaScript

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

JavaScript

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

十一 中介者模式

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

打個比方,軍火買賣雙方爲了安全起見,找了一個信任的中介來進行交易。買家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對多關係.

一段簡單的示例代碼:

JavaScript

十二 迭代器模式

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

JavaScript

obejct的迭代器:

JavaScript

十三 組合模式

組合模式又叫部分-整體模式,它將所有對象組合成樹形結構。使得用戶只需要操作最上層的接口,就可以對所有成員做相同的操作。
一個再好不過的例子就是jquery對象,大家都知道1個jquery對象其實是一組對象集合。比如在這樣一個HTML頁面

JavaScript

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

JavaScript

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

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

JavaScript

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

JavaScript

十四 備忘錄模式

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

JavaScript

十五 職責鏈模式
職責鏈模式是一個對象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就可以寫成享元.
僞代碼如下.

JavaScript

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

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

十七 狀態模式

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

回想下街頭霸王的遊戲。

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

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

JavaScript

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

JavaScript

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

 

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

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

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章