設計模式是命名、抽象和識別對可重用的面向對象設計有用的的通用設計結構。設計模式確定類和他們的實體、他們的角色和協作、還有他們的責任分配。
每一個設計模式都聚焦於一個面向對象的設計難題或問題。它描述了在其它設計的約束下它能否使用,使用它後的後果和得失。因爲我們必須最終實現我們的設計模式,所以每個設計模式都提供了例子,代碼來對實現進行闡釋.
雖然設計模式被描述爲面向對象的設計,它們基於那些已經被主流面嚮對象語言實現過的解決方案...”。
種類
設計模式可以被分成幾個不同的種類。在這個部分我們將分爲三類:創建型設計模式、結構設計模式、行爲設計模式。
創建型設計模式
創建型設計模式關注於對象創建的機制方法,通過該方法,對象以適應工作環境的方式被創建。基本的對象創建方法可能會給項目增加額外的複雜性,而這些模式的目的就是爲了通過控制創建過程解決這個問題。
屬於這一類的一些模式是:構造器模式(Constructor),工廠模式(Factory),抽象工廠模式(Abstract),原型模式(Prototype),單例模式(Singleton)以及 建造者模式(Builder)。
結構設計模式
結構模式關注於對象組成和通常識別的方式實現不同對象之間的關係。該模式有助於在系統的某一部分發生改變的時候,整個系統結構不需要改變。該模式同樣有助於對系統中某部分沒有達到某一目的的部分進行重組。
在該分類下的模式有:裝飾模式,外觀模式,享元模式,適配器模式和代理模式。
行爲設計模式
行爲模式關注改善或精簡在系統中不同對象間通信。
行爲模式包括:迭代模式,中介者模式,觀察者模式和訪問者模式。
下面我們通過分開介紹各個常用的設計模式,來加深對設計模式的理解。
構造器模式
構造器是一個當新建對象的內存被分配後,用來初始化該對象的一個特殊函數。對象構造器是被用來創建特殊類型的對象的,首先它要準備使用的對象,其次在對象初次被創建時,通過接收參數,構造器要用來對成員的屬性和方法進行賦值。
由於javascript不支持類的概念,所以必須通過構造器來使用new關鍵字初始化對象。一個基礎的構造器代碼如下:
- function Car( model, year, miles ) {
- this.model = model;
- this.year = year;
- this.miles = miles;
- this.toString = function () {
- return this.model + " has done " + this.miles + " miles";
- };
- }
- // 使用:
- // 我們可以示例化一個Car
- var civic = new Car( "Honda Civic", 2009, 20000 );
- var mondeo = new Car( "Ford Mondeo", 2010, 5000 );
- // 打開瀏覽器控制檯查看這些對象toString()方法的輸出值
- // output of the toString() method being called on
- // these objects
- console.log( civic.toString() );
- console.log( mondeo.toString() );
但是這樣的話,繼承起來比較麻煩,而且每個Car構造函數創建的對象中,toString之類的函數都會被重新定義。所以還是要利用原型,來實現最佳的構造器:
- function Car( model, year, miles ) {
- this.model = model;
- this.year = year;
- this.miles = miles;
- }
- // 注意這裏我們使用Note here that we are using Object.prototype.newMethod 而不是
- // Object.prototype ,以避免我們重新定義原型對象
- Car.prototype.toString = function () {
- return this.model + " has done " + this.miles + " miles";
- };
- // 使用:
- var civic = new Car( "Honda Civic", 2009, 20000 );
- var mondeo = new Car( "Ford Mondeo", 2010, 5000 );
- console.log( civic.toString() );
- console.log( mondeo.toString() );
工廠模式
一個工廠能提供一個創建對象的公共接口,我們可以在其中指定我們希望被創建的工廠對象的類型。說的簡單點,就像飲水機,要咖啡還是牛奶取決於你按哪個按鈕。
簡單工廠模式在創建ajax對象的時候可以體現出來,可以通過jquery中的$.ajax方法來理解,也可以通過我自己寫的一個ajax方法來理解,地址在:http://runjs.cn/code/j5dkikwu
- ajax("test002.txt",{
- type:"GET",
- data:{
- name:"liuf",
- age:23
- },
- onsuccess:function(responseText,xhr){
- document.getElementById("input").value=responseText;
- },
- onfail:function(){
- //document.write("fail");
- }
- });
ajax實際上就是一個工廠方法,至於到底是用get方法還是post方法,都由後面的代碼來決定。這就是前面所說的“一個工廠能提供一個創建對象的公共接口,我們可以在其中指定我們希望被創建的工廠對象的類型”。
單例模式
單例模式之所以這麼叫,是因爲它限制一個類只能有一個實例化對象。經典的實現方式是,創建一個類,這個類包含一個方法,這個方法在沒有對象存在的情況下,將會創建一個新的實例對象。如果對象存在,這個方法只是返回這個對象的引用。但是javascript本來就是無類的,所以簡單地來說,就是沒有就創建,有就不創建直接用。
那麼我們看看現實中的案例吧,點擊一個按鈕後出現一個遮罩層,這是一個常用的需求吧。代碼如下:
- 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,代碼如下:
- var mask;
- var createMask = function(){
- if ( mask ) return mask;
- else{
- mask = document,body.appendChild( document.createElement(div) );
- return mask;
- }
- }
這樣看起來不錯,但是mask作爲一個全局變量,是否會造成污染呢?所以最好的辦法如下:
- var createMask = function(){
- var mask;
- return function(){
- return mask || ( mask = document.body.appendChild( document.createElement('div') ) )
- }
- }()
這就是前面所說的“沒有就創建,有就不創建直接用”。沒錯,單例模式就是這麼簡單,設計模式其實並不難,編程中我們其實一直有用到,只是自己沒有發現罷了。
橋接模式
橋接模式就是將實現部分和抽象部分分離開來,以便兩者可以獨立的變化。在實現api的時候,橋接模式非常常用。
我們以javascript的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'
- }
現在我要調用兩個方法,我就可以使用一個更加高層的接口來調用:
- var getUserInfo = function(){
- var info = getName () + getSex ();
- return info;
- }
這樣就方便組裝,如果一開始就把兩個寫到一個函數中,那就不能夠只單獨調用其中一個了。
享元模式
享元模式是一個優化重複、緩慢和低效數據共享代碼的經典結構化解決方案。它的目標是以相關對象儘可能多的共享數據,來減少應用程序中內存的使用(例如:應用程序的配置、狀態等)。通俗的講,享元模式就是用來減少程序所需的對象個數。
舉一個例子,網頁中的瀑布流,或者webqq的好友列表中,每次往下拉時,都會創建新的div。那麼如果有很對div呢?瀏覽器豈不是卡死了?所以我們會想到一種辦法,就是把已經消失在視線外的div都刪除掉,這樣頁面就可以保持一定數量的節點,但是頻繁的刪除和添加節點,又會帶來很大的性能開銷。
這個時候就可以用到享元模式了,享元模式可以提供一些共享的對象以便重複利用。比如頁面中只能顯示10個div,那始終出現在用戶視線中的這10個div就可以寫成享元。
原理其實很簡單, 把剛隱藏起來的div放到一個數組中, 當需要div的時候, 先從該數組中取, 如果數組中已經沒有了, 再重新創建一個. 這個數組裏的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}";
適配器模式
適配器模式就是將一個類的接口轉換成客戶希望的另外一個接口。通俗一點的說,將像蘋果手機不能差在電腦機箱上,必須有一個轉換器,而這個轉換器就是適配器。
在程序裏適配器模式也經常用來適配2個接口, 比如你現在正在用一個自定義的js庫. 裏面有個根據id獲取節點的方法$id(). 有天你覺得jquery裏的$實現得更酷, 但你又不想讓你的工程師去學習新的庫和語法. 那一個適配器就能讓你完成這件事情.
- $id = function( id ){
- return jQuery( '#' + id )[0];
- }
這樣就不用再一個一個的修改了。
代理模式
代理模式就是把對一個對象的訪問,交給另一個代理對象來操作。說得通俗一點,程序員每天寫日報,日報最後會給總監審閱,但是如果所有人都直接發給總監,那總監就沒法工作了。所以每個人會把自己的日報發給自己的組長,再由組長轉發給總監。這個組長就是代理。
編程中用到代理模式的情況也不少,比如大量操作dom時,我們會先創建文檔碎片,再統一加到dom樹中。
示例如下:
中介者模式
中介者模式是觀察者模式中的共享被觀察者對象。在這個系統中的對象之間直接的發佈/訂閱關係被犧牲掉了,取而代之的是維護一個通信的中心節點。中介者模式和代理模式是有區別的,區別如下:
中介者對象可以讓各個對象之間不需要相互引用,從而使其耦合鬆散,而且可以獨立的改變透明之間的交互。通俗點講,銀行在存款人和貸款人之間也能看成一箇中介。存款人A並不關心他的錢最後被誰借走。貸款人B也不關心他借來的錢來自誰的存款。因爲有中介的存在,這場交易才變得如此方便。
在編程中,大名鼎鼎的MVC結構中的Controller不就是一箇中介者嗎?拿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' );
- } )
- })
觀察者模式
觀察者模式是這樣一種設計模式。一個被稱作被觀察者的對象,維護一組被稱爲觀察者的對象,這些對象依賴於被觀察者,被觀察者自動將自身的狀態的任何變化通知給它們。
當一個被觀察者需要將一些變化通知給觀察者的時候,它將採用廣播的方式,這條廣播可能包含特定於這條通知的一些數據。
當特定的觀察者不再需要接受來自於它所註冊的被觀察者的通知的時候,被觀察者可以將其從所維護的組中刪除。
通俗點理解,就是面試官是被觀察者,而等待通知的人是觀察者。
javascript中平時接觸的dom事件,其實就是一種觀察者模式的體現:
- div.onclick = function click (){
- alert ( ''click' )
- }
只要訂閱了div的click事件,當點擊div的時候,click函數就會執行。
觀察者模式可以很好的實現連個模塊之間的解耦,假如一個多人合作的項目,我負責Map和Gamer模式:
- loadImage( imgAry, function(){
- Map.init();
- Gamer.init();
- } )
- <p>而別人負責loadImage方法的維護,如果有一天,我要加上一個Sound模塊,而我無權動用別人的代碼,只能空空等待別人回來添加:</p><pre name="code" class="html">loadImage( imgAry, function(){
- Map.init();
- Gamer.init();
- Sount.init();
- } )
所以我們可以做如下改動:
- loadImage.listen( ''ready', function(){
- Map.init();
- })
- loadImage.listen( ''ready', function(){
- Gamer.init();
- })
- loadImage.listen( ''ready', function(){
- Sount.init();
- })
loadImage完成之後,不用再關心未來會發送什麼,接下來它只需要發送一個信號:
- loadImage.trigger( ‘ready’ );
所有監聽ready事件的對象就都會收到通知。這就是觀察者模式的應用場景。