《JavaScript設計模式與開發實踐》閱讀摘要

《JavaScript設計模式與開發實踐》作者:曾探

系統的介紹了各種模式,以及js中的實現、應用,以及超大量高質量代碼,絕對值得一讀


面向對象的js

靜態類型:編譯時便已確定變量的類型
優點:

編譯時就能發現類型不匹配的錯誤,編輯器可以幫助我們提前避免程序在運行中可能發生的一些錯誤;編譯器可以針對數據類型對程序進行一些優化工作;

缺點:

迫使程序員按照契約來編寫;類型的聲明會增加更多的代碼;

動態類型:程序運行的時候,變量被賦予某個值之后,才會具有某種類型
優點:

編寫的代碼數量更少,看起來也更簡潔,程序員可以把更多精力放在業務邏輯;給編碼帶來了很大的靈活性,無需進行類型檢測,可以嘗試調用任何對象的任意方法,無需考慮它原本是否被設計為擁有該方法,建立在鴨子類型上。

缺點:

無法保證變量的類型,從而在程序運行期可能發生跟類型相關的錯誤;

鴨子類型:

“如果它走起路來像鴨子,叫起來也像鴨子,那么它就是鴨子。”

鴨子類型指導我們只關注對象的行為,而不關注對象本身,即灌輸HAS-A,而不是IS-A。利用鴨子類型的思想,不必借助超類型的幫助,就可以輕松實現:“面向接口編程,而不是面向實現編程。”例如:一個對象若有push和pop方法,并且提供了正確的實現,他就可以被當成棧來使用。

多態:
實際含義:

同一操作作用于不同的對象上面,可以產生不同的解釋和不同的執行結果。換句話說,給不同對象發送同一個消息的時候,這些對象會根據這個消息分別給出不同的反饋。

本質:

實際上時把“做什么”和“誰去做”分離開來,消除類型之間的耦合關系,js對象的多態性時與生俱來的。

作用:

把過程化的條件分支語句轉化為對象的多態性,從而消除這些分支語句。

靜態類型的多態:

通過向上轉型:當給一個類變量賦值時,這個變量的類既可以使用這個類本身,也可以使用這個類的超類。使用繼承來得到多態效果,是讓對象表現出多態性的最常用手段:包括實現繼承、接口繼承。

js的多態:

js的變量類型在運行期是可變的,一個對象可以表示不同類型的對象,js對象的多態性是與生俱來的。

封裝:
包含:

封裝數據、封裝實現、封裝類型、封裝變化。

封裝數據:

通常是由語法解析實現(private、public、protected),js只能通過變量的作用域實現,并且只能模擬出public和private這兩種封裝性。

封裝實現:

對象內部的變化對其他對象是透明不可見的;對象對它自己的行為負責;其他對象不關心它的內部實現;封裝使得對象之間的耦合變松散,對象之間只通過暴露的API接口來通信。

封裝類型:

靜態語言中一種重要的封裝方式,一般通過抽象類和接口來進行,把對象真正的類型隱藏在抽象類或者接口之后,相比對象的類型,客戶更關心對象的行為。封裝類型方面,js沒有能力,也沒有必要做得更多。

封裝變化:

通過封裝變化的方式,把系統中穩定不變的部分和容易改變的部分隔離開來,在系統的演變過程中,我們只需要替換那些容易變化的部分,如果這些部分是已經封裝好的,替換起來也相對容易,這可以最大程度的保證程序的穩定性和可拓展性。

原型編程:

以類為中心的面向對象編程語言中,類和對象的關系可以想象成鑄模和鑄件的關系,對象總是從類中創建而來。原型編程的思想中,類并不是必需的,對象是通過克隆另外一個對象得到的。

原型模式

定義:

既是一種設計模式也被稱為一種編程范型。原型模式是用于創建對象的一種模式,不關心對象的具體類型,找到一個對象,通過克隆來創建一個一摸一樣的對象。

實現關鍵:

語言本身是否提供了clone方法,es5提供了Object.create方法,可以用來克隆對象。

目的:

提供了一種便捷的方式去創建某個類型的對象。

原型繼承的本質:

基于原型鏈的委托機制。

委托機制:

當對象無法響應某個請求時,會把該請求委托給它的原型。

原型編程范型基本規則:
  • 所有的數據都是對象
  • 要得到一個對象,不是通過實例化類,而是找到一個對象作為原型并克隆它
  • 對象會記住它的原型
  • 如果對象無法響應某個請求,它會把這個請求委托給它自己的原型
js中的原型繼承
所有的數據都是對象:

設計者本意,除了undefined之外,一切都應該是對象,所以存在“包裝類”。js不能說所有的數據都是對象,但可以說絕大部分數據都是對象,js中存在Object.prototype對象,其他對象追根溯源都克隆于這個根對象,Object.prototype是它們的原型。

要得到一個對象,不是通過實例化類,而是找到一個對象作為原型并克隆它:

js語言中,我們不需要關系克隆的細節,引擎內部負責實現,只要顯示的調用var obj1 = new Object()或者var obj2 = {}。引擎內部會從Object.prototype上克隆一個對象出來。用new運算符來創建對象的多城,實際上也只是先克隆Object.prototype對象,再進行一些其他額外操作。

對象會記住它的原型:

js給對象提供了一個名為proto的隱藏屬性,默認會指向它的構造器的原型對象,它就是對象跟它的原型聯系起來的紐帶。

如果對象無法響應某個請求,它會把這個請求委托給它的原型:

原型鏈查找

原型繼承的未來:

設計模式在很多時候都體現了語言的不足之處


this、call、和apply

this:

總是指向一個對象,而具體指向哪個對象是在運行時基于執行環境動態綁定的,而非函數被聲明時的環境。

this的指向:
  • 作為對象的方法調用
  • 作為普通的函數調用
  • 構造器調用
  • Function.prototype.call或Function.prototype.apply調用
  1. 作為對象的方法被調用時,this指向該對象
  2. 作為普通函數調用,this總是指向全局對象,在瀏覽器中全局對象為window,在node.js中全局對象為global,嚴格模式下為undefined
  3. 構造器調用,this通常情況下指向返回的對象
  4. Function.prototype.call或Function.prototype.apply調用動態的綁定this到傳入的第一個參數
call和apply的區別:

傳入參數形式不同,它們第一個參數都是指定函數體內this對象的指向,apply第二個參數為一個帶下表的集合,可以是數組或者類數組,call第二個參數開始,每個參數依次被傳入函數。apply比call的使用率更高,call是包裝在apply上面的語法糖,如果我們明確的知道函數接受多少個參數,并且想一目了然地表達形參和實參的對應關系,適合使用call來傳送。

call和apply的用途:
  1. 改變this的指向
  2. Function.prototype.bind:
Function.prototype.bind = function ( context ) {
       var self = this;
        return function () {
           return self.apply( context, arguments );
        }
};
  1. 借用其他對象的方法:借用構造函數、對類數組甚至對象(對象本身要可以存取屬性、length屬性可讀寫)使用數組的方法

閉包和高階函數

js是一門完整的面向對象的編程語言,同時也擁有許多函數式語言的特性。

變量的作用域:

變量的有效范圍,在函數聲明變量時沒有帶關鍵字var就會變成全局變量,使用了var時稱為局部變量,只有在該函數內部才能訪問到這個變量,在函數外面時訪問不到的。js中函數可以用來創造函數作用域。在函數里面可以看到外面的變量,而在函數外面無法看到函數里面的變量,這是因為在函數中搜索一個變量的時候,如果該函數內并沒有聲明這個變量,那么搜索的過程會隨著代碼執行環境創建的作用域鏈往外層逐層搜索,一直搜索到全局對象為止。

變量的生存周期:

全局變量的生存周期是永久的,除非主動銷毀。在函數內使用var聲明的局部變量,在函數退出時,這些局部變量記失去了它們的價值,會隨著函數調用的結束而被銷毀。

閉包的作用:

因為對外部作用域的引用可以阻止外部的作用域被銷毀,延長了局部變量的生命周期、可以把每次循環中的i值都封閉起來,使循環綁定事件符合我們的預期

閉包的更多作用:
封裝變量:

提煉函數時代碼重構中的一種常見技巧。如果在一個大函數中有一些代碼塊能夠獨立出來,常常把這些代碼塊封裝在獨立的小函數里面。獨立的小函數有助于代碼復用,如果有良好的命名,本身也起到了注釋的效果,如果這些小函數不需要在程序的其他地方使用,最好是把它們用閉包封閉起來。

閉包和面向對象設計:

對象以方法的形式包括了過程,閉包在過程中以環境的形式包含了數據。通常用面向對象思想能實現的功能,用閉包也能實現,反之亦然。

用閉包實現命令模式:

命令模式的意圖是把請求封裝為對象,從而分離請求的發起者和請求的接收者(執行者)之間的耦合關系。

閉包與內存管理:

解決對象間循環引用帶來的內存泄漏問題,只需要把循環引用中的變量設為null即可。將變量設置為null意味著切斷變量與它此前引用的值之間的連接。當垃圾收集器瑕疵運行時,就會刪除這些值并回收它們占用的內存

高階函數:
定義:

滿足下列條件之一的函數:

  • 函數可以作為參數被傳遞
  • 函數可以作為返回值輸出
函數作為參數傳遞:

這代表著我們可以抽離一部分容易變化的業務邏輯,把這部分業務邏輯放在函數參數中,這樣依賴可以分離業務代碼中變化與不變的部分。例如:

  1. 回調函數
    異步請求、當一個函數不適合執行一些請求時,可以把這些請求封裝成一個函數,并把它作為參數傳遞給另一個函數,“委托”給另外一個函數來執行。
  2. Array.prototype.sort
    Array.prototype.sort接受一個函數當作參數,這個函數里封裝了數組元素的排序規則。從Array.prototype.sort的使用可以看到,我們的目的是對數組進行排序,這是不變的部分;從而使用什么規則去排序,則是可變的部分。把可變的部分封裝在函數參數里,動態傳入,使這個方法稱為了一個非常靈活的方法。
函數作為返回值輸出:
  1. 判斷數據的類型
var isType = function(type){
    return function( obj ) {
    return Object.prototype.toString.call( obj ) === ‘[object ‘ + type + ‘]’;
    }
};
var isString = isType(‘String’);
var isArray = isType(‘Array’);
var isNumber = isType(‘Number’);

2.getSingle

var getSingle = function ( fn ) {
    var ret;
    return function ( ) {
        return ret || (ret = fn.apply ( this, arguments ) );
    };
};
高階函數實現AOP

AOP(面向切面編程)的主要作用是吧一些跟核心業務邏輯模塊無關的功能抽離出來,這些跟業務邏輯無關的功能通常包括日志統計、安全控制、異常處理等。把這些功能抽離出來之后,再通過“動態織入”的方式摻入業務邏輯模塊中。優點首先是可以保持業務邏輯模塊的純凈和高內聚性,其次是可以很方便的復用日志統計等功能模塊。js中實現AOP更簡單,這是js與生俱來的能力,這種使用AOP的方式給函數添加職責,也是js語言中一種非常特別和巧妙的裝飾者模式實現。

高階函數的其他應用:
  1. currying:
    currying又稱部分求職。一個currying的函數首先會接受一些參數,接受了這些參數之后,該函數并不會立即求職,而是繼續返回另外一個函數,剛才傳入的參數在函數形成閉包中被保存起來。待到函數真正需要求職的時候,之前傳入的所有參數都會被一次性用于求值。
  2. uncurrying:
    js中,當我們調用對象的某個方法時,其實不用去關系改對象原本是否被設計為擁有這個方法,這是動態語言的特點,也是常說的鴨子類型思想。
  3. 函數節流:
    函數被頻繁調用的場景:window.onresize事件、mousemove事件、上傳進度
    函數節流的原理:借助setTimeout來完成
    函數節流的代碼實現:
var throttle = function ( fn, interval ) {
    var _self = fn,
          timer,
          firstTime = true;

    return function () {
          var args = arguments,
          _me = this;

        if ( fisrtTime ) {
            _self.apply( _me, args );
            return firstTime = false;
        }

        if ( timer ) {
            return false;
        }

        timer = setTimeout ( function ( ) [
            clearTimeout ( timer );
            timer = null;
            _self.apply ( _me, args );

        }, interval || 500 );
    };
};
window.onresize = throttle ( function ( ) {
    console.log ( 1 );
}, 500 );
  1. 分時函數
    使用函數、定時器讓一個大任務分成多個小任務
  2. 惰性加載函數
    在第一次進入條件分支之后,在函數內部會重寫這個函數,重寫之后的函數就是符合當前瀏覽器環境的函數。

單例模式

實現一些只需要一個的對象,比如線程池、全局緩存、window對象等

實現單例模式:

用一個變量來標志當前是否已經為某個類創建過對象,如果是,則在下一次獲取該類的實例時,直接返回之前創建的對象。

透明的單例模式:

用戶從這個類中創建對象的時候,可以像使用其他任何普通的類一樣。

用代理實現單例模式:
    var CreateDiv = function (html) {
        this.html = html;
        this.init();
    };

    CreateDiv.prototype.init = function () {
        var div = document.createElement('div');
        div.innerHTML = this.html;
        document.body.appendChild(div);
    };

    var ProxySingleCreateDiv = (function () {
        var instance;
        return function (html) {
            if (!instance) {
                instance = new CreateDiv(html);
            }
            return instance;
        }
    })();
    var a = new ProxySingleCreateDiv('seven1');
    var b = new ProxySingleCreateDiv('seven2');
    alert(a === b);
js中的單例模式:

可以將全局變量當作單例模式來使用,但是全局變量會污染命名空間。可以使用以下幾種方法避免全局空間的污染:

  1. 使用命名空間
    不會杜絕全局變量,可以減少全局變量的數量。使用對象字面量的方式。
  2. 使用閉包封裝私有變量
    把變量封裝在閉包的內部,只暴露一些借口跟外界通信。
惰性單例

惰性單例是指在需要的時候才創建對象實例。

通用的惰性單例
var getSingle = function( fn ) {
    var result ;
    return function ( ) {
        return result || ( result = fn.apply ( this, arguments ) );
    } 
};
~~~
***
####策略模式
實現一個功能有多個方案可以選擇
#####定義:
定義一系列的算法,把它們一個個封裝起來,并且使它們可以互相替換。
#####策略模式的程序組成:
第一個部分是一組策略類,策略類封裝了具體的算法,并負責具體的計算過程。第二個部分是環境類Context,Context接受客戶的請求,隨后把請求委托給某一個策略類。

#####js版本的策略模式:
在js語言中,函數也是對象,所以更簡單和直接的做法是把策略和Context定義成一個函數。

#####多態在策略模式中的體現:
所有跟算法有關的邏輯不再放在Context中,而是分布在各個策略隊形中。當我們對這些策略對象發出請求時,它們會返回各自不同的結果,這正是對象多態性的體現,也是“它們可以互相替換”的目的。

#####使用策略模式實現緩動動畫
原理:js實現動畫效果的原理跟動畫片的制作一樣,js通過連續改變元素的某個CSS屬性,來實現動畫效果。
思路和準備工作:
運動之前,需要記錄一些有用的信息,至少包括:
* 動畫開始時,小球所在的原始位置;
* 小球移動的目標位置
* 動畫開始時的準確時間點
* 小球運動的持續時間

通過定時器,把動畫已消耗的時間、小球原始位置、小球目標位置和動畫持續的總時間傳入緩動算法。該算法會通過這幾個參數,計算出小球當前應該所在的位置。最后再更新該div的CSS屬性,小球就能順利的動起來了。
~~~
var Animate = function( dom ){
        this.dom = dom; // 進行運動的dom 節點
        this.startTime = 0; // 動畫開始時間
        this.startPos = 0; // 動畫開始時,dom 節點的位置,即dom 的初始位置
        this.endPos = 0; // 動畫結束時,dom 節點的位置,即dom 的目標位置
        this.propertyName = null; // dom 節點需要被改變的css 屬性名
        this.easing = null; // 緩動算法
        this.duration = null; // 動畫持續時間
    };
    Animate.prototype.start = function( propertyName, endPos, duration, easing ){
        this.startTime = +new Date; // 動畫啟動時間
        this.startPos = this.dom.getBoundingClientRect()[ propertyName ]; // dom 節點初始位置
        this.propertyName = propertyName; // dom 節點需要被改變的CSS 屬性名
        this.endPos = endPos; // dom 節點目標位置
        this.duration = duration; // 動畫持續事件
        this.easing = tween[ easing ]; // 緩動算法
        var self = this;
        var timeId = setInterval(function(){ // 啟動定時器,開始執行動畫
            if ( self.step() === false ){ // 如果動畫已結束,則清除定時器
                clearInterval( timeId );
            }
        }, 19 );
    };

    Animate.prototype.step = function(){
    var t = +new Date; // 取得當前時間
    if ( t >= this.startTime + this.duration ){ // (1)
        this.update( this.endPos ); // 更新小球的CSS 屬性值
        return false;
    }
    var pos = this.easing( t - this.startTime, this.startPos, this.endPos - this.startPos, this.duration );
    // pos 為小球當前位置
        this.update( pos ); // 更新小球的CSS 屬性值
    };

    Animate.prototype.update = function( pos ){
        this.dom.style[ this.propertyName ] = pos + 'px';
    };

    var div = document.getElementById( 'div' );
    var animate = new Animate( div );
    animate.start( 'left', 500, 1000, 'strongEaseOut' );
~~~
#####更廣義的“算法”
把算法的含義擴散開來,使策略模式也可以用來封裝一系列的“業務規則”,只要這些業務規則指向的目標一致,并且可以被替換使用,我們就可以用策略模式來封裝它們。

#####策略模式的優點
* 策略模式利用組合、委托、和多態等技術和思想,可以有效地避免多重條件選擇語句
* 策略模式提供了對開放-封閉原則的完美支持,將算法封裝在獨立strategy中,使得它們易于切換,易于理解,易于擴展
* 策略模式中的算法也可以復用在系統的其他地方,從而避免許多重復的復制和粘貼工作
* 在策略模式中利用組合和委托讓Context擁有執行算法的能力,這也是繼承的一種更輕便的替代方案

#####策略模式的缺點:
首先,使用策略模式會在程序中增加許多策略類或者策略對象,但實際上這比把它們負責的邏輯堆在Context中要好
其次,要使用策略模式,必須了解所有的strategy,必須了解各個strategy之間的不同點,這樣才能選擇一個合適的strategy。

#####一等函數對象與策略模式:
在js中除了使用類來封裝算法和行為之外,使用函數當然也是一種選擇。這些“算法”可以被封裝在函數中并且四處傳遞,也就是我們常說的“高階函數”
***
####代理模式
代理模式是為一個對象提供一個待用品或占位符,以便控制對它的訪問。
#####現實場景例子:
明星的經紀人代替明星協商。
#####保護代理和虛擬代理:
控制不同權限的對象對目標對象的訪問,叫作保護代理;把一些開銷很大的對象,延遲到真正需要它的時候再去創建,叫作虛擬代理。js中不容易實現保護代理,虛擬代理是最常用的一種代理模式。

#####代理的意義:
######單一職責原則:
一個類(也包括對象和函數)應該僅有一個引起它變化的原因。如果一個對象承擔了多項職責,就意味著這個對象將變得巨大,面向對象設計鼓勵將行為分布到顆粒度的對象之中,如果一個對象承擔的職責過多,等于把這些職責耦合到了一起,這種耦合會導致脆弱和低內聚的設計。當變化發生時,設計可能會遭到意外的破壞。

######代理和本體接口的一致性:
優點:
* 用戶可以放心地請求代理,他只關心能否得到想要的結果
* 在任何使用本體的地方都可以替換成使用代理

#####虛擬代理實現圖片加載
~~~
    var myImage = (function(){
        var imgNode = document.createElement( 'img' );
        document.body.appendChild( imgNode );
        return function( src ){
            imgNode.src = src;
        }
    })();
    var proxyImage = (function(){
        var img = new Image;
        img.onload = function(){
            myImage( this.src );
        }
        return function( src ){
            myImage( 'file:// /C:/Users/svenzeng/Desktop/loading.gif' );
            img.src = src;

        }
    })();
    proxyImage( 'http:// imgcache.qq.com/music// N/k/000GGDys0yA0Nk.jpg' );
~~~
#####緩存代理:
緩存代理可以為一些開銷大的運算結果提供暫時的存儲,在下次運算時,如果傳遞進來的參數跟之前一致,則可以直接返回前面的運算結果。

#####其他代理模式:
* 防火墻代理
* 遠程代理
* 保護代理
* 智能引用代理
* 寫時復制代理(虛擬代理的變體)
***
####迭代器模式:
提供一種方法順序訪問一個聚合對象中的各個元素,而又不需要暴露該對象的內部表示。迭代器模式可以把迭代的過程從業務邏輯中分離出來,在使用迭代器模式之后,即使不關心對象的內部構造,也可以按順序訪問其中的每個元素。
#####例子:Array.prototype.forEach
#####內部迭代器和外部迭代器:
* 內部迭代器:each函數的內部已經定義好了迭代規則,它完全接手整個迭代過程,外部只要一次引用。缺點:迭代規則已經被提前規定,無法靈活更改
* 外部迭代器:必須顯式地請求迭代下一個元素,增加了調用的復雜度,但也增強了迭代器的靈活性,我們可以手工控制迭代的過程或者順序。

#####迭代類數組對象和字面量對象:
無論內部迭代器還是外部迭代器,只要迭代的聚合對象擁有length屬性而且可以用下標訪問,那它就可以被迭代。
~~~
    var each = function( ary, callback ){
        for ( var i = 0, l = ary.length; i < l; i++ ){
            if ( callback( i, ary[ i ] ) === false ){ // callback 的執行結果返回false,提前終止迭代
                break;
            }
        }
    };

    each( [ 1, 2, 3, 4, 5 ], function( i, n ){
        if ( n > 3 ){ // n 大于3 的時候終止循環
            return false;
        }
        console.log( n ); // 分別輸出:1, 2, 3
    });
~~~
#####發布—訂閱模式(觀察者模式)
無論MVC還是MVVM都少不了發布-訂閱模式,js本身也是一門基于事件驅動的語言。
#####現實場景例子:把電話留給售樓處,一旦有新房會電話通知。
#####優點:
時間上解耦、對象之間解耦。
#####缺點:
創建訂閱者本身要消耗一定的時間和內存、過度使用導致對象和對象之間的必要聯系也將被深埋導致程序難以維護和理解
#####作用:
* 可以廣泛應用于異步編程中,代替傳遞回調函數。通過訂閱事件可以忽略運行期間的狀態,只需要關注事件本身。
* 取代對象之間硬編碼的通知機制,一個對象不用顯式地調用另一個對象的某個接口。讓兩個對象松耦合地聯系在一起,雖然不清楚彼此間的細節,但這不影響它們之間相互通信。
#####js實現發布-訂閱模式的便利性:
注冊回調函數代替傳統的發布-訂閱模式,更加優雅、簡單
~~~
//所以我們把發布—訂閱的功能提取出來,放在一個單獨的對象內:
var event = {
    clientList: [],
    listen: function( key, fn ){
        if ( !this.clientList[ key ] ){
            this.clientList[ key ] = [];
        }
            this.clientList[ key ].push( fn ); // 訂閱的消息添加進緩存列表
        },
        trigger: function(){
            var key = Array.prototype.shift.call( arguments ), // (1);
            fns = this.clientList[ key ];
            if ( !fns || fns.length === 0 ){ // 如果沒有綁定對應的消息
                return false;
            }
            for( var i = 0, fn; fn = fns[ i++ ]; ){
                fn.apply( this, arguments ); // (2) // arguments 是trigger 時帶上的參數
            }
        }
    };

    var installEvent = function( obj ){
        for ( var i in event ){
            obj[ i ] = event[ i ];
        }
    };
~~~
***
####命令模式
最簡單和優雅的模式之一,命令模式中的命令(command)指的是一個執行某些特定事情的指令。
#####現實場景例子:點菜。
#####應用場景:
有時需要向某些對象發送請求,但是不知道請求的接受者是誰,也不知道請求的操作是什么。
#####js中的命令模式:
js作為將函數作為一等對象的語言,跟策略模式一樣,命令模式也早已融入到了js語言中,可以用高階函數非常方便的實現命令模式,是一種隱式的模式。
~~~
        var setCommand = function( button, func ){
            button.onclick = function(){
                func();
            }
        };
        var MenuBar = {
            refresh: function(){
                console.log( '刷新菜單界面' );
            }
        };
        var RefreshMenuBarCommand = function( receiver ){
            return function(){
                receiver.refresh();
            }
        };
        var refreshMenuBarCommand = RefreshMenuBarCommand( MenuBar );
        setCommand( button1, refreshMenuBarCommand );
~~~
#####智能命令與傻瓜命令:
一般來說命令模式都會在command對象中保存一個接收者來負責真正執行客戶的請求,這種情況下命令對象是“傻瓜式”的,它只負責把客戶的請求轉交給接受者來執行,這種模式的好處是請求發起者和接受者之間盡可能地得到了解耦。“聰明”的命令對象可以直接實現請求,這樣以來就不再需要接受者的存在,這種“聰明”的命令對象也叫作智能命令。
***
####組合模式
#####含義:
用小的子對象構建更大的對象,這些小的子對象本身也許是由更小的對象構成的。
#####用途:
1. 表示樹形結構,非常方便的描述對象部分-整體層次結構
2. 利用對象多態性統一對待組合對象和單個對象

#####一些值得注意的地方
1. 組合模式不是父子關系
2. 對葉對象操作的一致性
3. 雙向映射關系
4. 用職責鏈提高組合模式性能

~~~
<html>
<body>
    <button id="button">按我</button>
</body>
<script>
    var MacroCommand = function(){
        return {
            commandsList: [],
            add: function( command ){
                this.commandsList.push( command );
            },
            execute: function(){
                for ( var i = 0, command; command = this.commandsList[ i++ ]; ){
                    command.execute();
                }
            }
        }
    };
    var openAcCommand = {
        execute: function(){
            console.log( '打開空調' );
        }
    };
/**********家里的電視和音響是連接在一起的,所以可以用一個宏命令來組合打開電視和打開音響的命令
*********/
var openTvCommand = {
    execute: function(){
        console.log( '打開電視' );
    }
};
var openSoundCommand = {
    execute: function(){
        console.log( '打開音響' );
    }
};
var macroCommand1 = MacroCommand();
macroCommand1.add( openTvCommand );
macroCommand1.add( openSoundCommand );
/*********關門、打開電腦和打登錄QQ 的命令****************/
var closeDoorCommand = {
    execute: function(){
        console.log( '關門' );
    }
};
var openPcCommand = {
    execute: function(){
        console.log( '開電腦' );
    }
};
var openQQCommand = {
    execute: function(){
        console.log( '登錄QQ' );
    }
};
var macroCommand2 = MacroCommand();
macroCommand2.add( closeDoorCommand );
macroCommand2.add( openPcCommand );
macroCommand2.add( openQQCommand );
/*********現在把所有的命令組合成一個“超級命令”**********/
var macroCommand = MacroCommand();
macroCommand.add( openAcCommand );
macroCommand.add( macroCommand1 );
macroCommand.add( macroCommand2 );
/*********最后給遙控器綁定“超級命令”**********/
var setCommand = (function( command ){
    document.getElementById( 'button' ).onclick = function(){
        command.execute();
    }
})( macroCommand );
</script>
</html>
~~~

***
####模版方法模式
#####定義:
一種只需要使用繼承就可以實現的非常簡單的模式
~~~
    var Coffee = function(){};
    Coffee.prototype = new Beverage();

    Coffee.prototype.brew = function(){
        console.log( '用沸水沖泡咖啡' );
    };
    Coffee.prototype.pourInCup = function(){
        console.log( '把咖啡倒進杯子' );

    };
    Coffee.prototype.addCondiments = function(){
        console.log( '加糖和牛奶' );
    };
    var Coffee = new Coffee();
    Coffee.init();

    Beverage.prototype.init = function(){
        this.boilWater();
        this.brew();
        this.pourInCup();
        this.addCondiments();
    };

    var Tea = function(){};
    Tea.prototype = new Beverage();
    Tea.prototype.brew = function(){
        console.log( '用沸水浸泡茶葉' );
    };
    Tea.prototype.pourInCup = function(){
        console.log( '把茶倒進杯子' );
    };
    Tea.prototype.addCondiments = function(){
        console.log( '加檸檬' );
    };
    var tea = new Tea();
    tea.init();
~~~
#####組成:
由兩部分結構組成:第一部分是抽象父類,第二部分是具體的實現子類。通常在抽象父類中封裝了子類的算法框架,包括實現一些公共方法以及封裝子類中所有方法的執行順序。子類通過繼承這個抽象類,也繼承了整個算法結構,并且可以選擇重寫父類的方法。
#####抽象類:
不應被實例化,用來被某些具體類繼承的。用于向上轉型、為子類定義公共接口。
#####抽象方法:
沒有具體的實現過程,當子類繼承這個抽象類時,必須重寫抽象方法
#####具體方法:
具體實現方法 

#####js沒有抽象類的缺點和解決方案
抽象類的一個作用時隱藏對象的具體類型,因為js時一門“類型模糊”的語言,所以隱藏對象在js中并不總要。使用原型繼承來墨跡傳統的類繼承時,并沒有編譯器幫助我們進行任何形式的檢查,我們也沒有辦法保證子類會重寫父類中的“抽象方法”
######解決方案:
* 第一種方案:使用鴨子類型來模擬設備接口檢查,以便確保子類中確實重寫了父類的方法;
* 第二種方案:讓父類的方法直接拋出一個異常,入股因為粗心忘記改寫,至少我們會在程序運行時得到一個錯誤。

#####鉤子方法:防止鉤子是隔離變化的一種常見手段。我們在父類中容易變化的地方放置鉤子,鉤子有一個默認的實現,究竟要不要“掛鉤“,由子類自行決定

#####好萊塢原則:
允許底層組件將自己掛鉤到高層組件中,高層組件會決定什么時候、以何種方式去使用這些底層組件。模版方法模式是好萊塢原則的一個典型使用場景,它與好萊塢原則的聯系非常明顯,當我們用模版方法編寫一個程序時,就意味著子類放棄了對自己的控制權,而是改為父類通知子類,哪些方法應該在什么時候被調用。作為子類,只負責提供一些設計上的細節。還適用于其他模式和場景,例如發布—訂閱模式和回調函數。
***
####享元(flyweight)模式
一種用于性能優化的模式,fly在這里是蒼蠅的意思,意為蠅量級,核心是運用共享技術來有效支持大量細粒度的對象。
#####現實場景例子:模特換不同的衣服拍照。
#####內部狀態和外部狀態:
* 內部狀態存儲于對象內部
* 內部狀態可以被一些對象共享
* 內部狀態獨立于具體的場景,通常不會改變
* 外部狀態取決于具體的場景,并根據場景而變化,外部狀態不能被共享

#####享元模式的適用性
* 一個程序使用了大量的相似對象
* 由于使用了大量對象,造成很大的內存開銷
* 對象的大多數狀態都可以變成外部狀態
* 剝離出對象的外部狀態之后,可以用相對較少的共享對象取代大量對象

#####享元模式的關鍵:
把內部狀態和外部狀態分離開來。有多少內部狀態的組合,系統便最多存在多少個共享對象,而外部狀態則儲存在共享對象的外部,在必要時傳入共享對象來組裝成一個完整的對象。
也可以用對象池+事件委托來代替實現
~~~
var Upload = function( uploadType){
        this.uploadType = uploadType;
    };

    Upload.prototype.delFile = function( id ){
        uploadManager.setExternalState( id, this ); // (1)
        if ( this.fileSize < 3000 ){
            return this.dom.parentNode.removeChild( this.dom );
        }

        if ( window.confirm( '確定要刪除該文件嗎? ' + this.fileName ) ){
            return this.dom.parentNode.removeChild( this.dom );
        }
    }


    var UploadFactory = (function(){
        var createdFlyWeightObjs = {};
        return {
            create: function( uploadType){
                if ( createdFlyWeightObjs [ uploadType] ){
                    return createdFlyWeightObjs [ uploadType];
                }
                return createdFlyWeightObjs [ uploadType] = new Upload( uploadType);
            }
        }
    })();

    var uploadManager = (function(){
        var uploadDatabase = {};
        return {
            add: function( id, uploadType, fileName, fileSize ){
                var flyWeightObj = UploadFactory.create( uploadType );
                var dom = document.createElement( 'div' );
                dom.innerHTML =
                '<span>文件名稱:'+ fileName +', 文件大小: '+ fileSize +'</span>' +
                '<button class="delFile">刪除</button>';
                dom.querySelector( '.delFile' ).onclick = function(){
                    flyWeightObj.delFile( id );
                }

                document.body.appendChild( dom );
                uploadDatabase[ id ] = {
                    fileName: fileName,
                    fileSize: fileSize,
                    dom: dom
                };
                return flyWeightObj ;
            },
            setExternalState: function( id, flyWeightObj ){
                var uploadData = uploadDatabase[ id ];
                for ( var i in uploadData ){
                    flyWeightObj[ i ] = uploadData[ i ];
                }
            }
        }
    })();

    var id = 0;
    window.startUpload = function( uploadType, files ){
        for ( var i = 0, file; file = files[ i++ ]; ){
            var uploadObj = uploadManager.add( ++id, uploadType, file.fileName, file.fileSize );
        }
    };
~~~
***
####職責鏈模式
使多個對象都有機會處理請求,從而避免請求的發送者和接收者之間的耦合關系,將這些對象連成一條鏈,并沿著這條鏈傳遞該請求,直到有一個對象處理它為止。
#####現實場景例子:
公交車人太多了,找不到售票員,通過一個個人將錢遞給售票員
#####例子:
作用域鏈、原型鏈、dom節點事件冒泡
#####最大優點:
請求發送者只需要知道鏈中的第一個節點,從而弱化了發送者和接受者之間的強聯系。
#####缺點:
使程序中多了一些節點對象,可能在某一次請求傳遞的過程中,大部分節點沒有起到實質性的作用,它們的作用僅僅是讓請求傳遞下去,從性能方面考慮,我們要避免過長的職責鏈帶來的性能損耗
#####小結:
js開發中,職責鏈模式是最容易被忽視的模式之一。職責鏈模式可以很好地幫助我們管理代碼,降低發起請求的對象和處理請求的對象之間耦合性。職責鏈匯中的節點數量和順序是可以自由變化的。
~~~
var order500 = function( orderType, pay, stock ){
        if ( orderType === 1 && pay === true ){
            console.log( '500 元定金預購,得到100 優惠券' );
        }else{
            return 'nextSuccessor'; // 我不知道下一個節點是誰,反正把請求往后面傳遞
        }
    };

    var order200 = function( orderType, pay, stock ){
        if ( orderType === 2 && pay === true ){
            console.log( '200 元定金預購,得到50 優惠券' );
        }else{
            return 'nextSuccessor'; // 我不知道下一個節點是誰,反正把請求往后面傳遞
        }
    };

    var orderNormal = function( orderType, pay, stock ){
        if ( stock > 0 ){
            console.log( '普通購買,無優惠券' );
        }else{
            console.log( '手機庫存不足' );
        }
    };

    // Chain.prototype.setNextSuccessor 指定在鏈中的下一個節點
    // Chain.prototype.passRequest 傳遞請求給某個節點
    var Chain = function( fn ){
        this.fn = fn;
        this.successor = null;
    };

    Chain.prototype.setNextSuccessor = function( successor ){
        return this.successor = successor;
    };

    Chain.prototype.passRequest = function(){

        var ret = this.fn.apply( this, arguments );
        if ( ret === 'nextSuccessor' ){
            return this.successor && this.successor.passRequest.apply( this.successor, arguments );
        }
        return ret;
    };
~~~
***
####中介者模式
#####作用:
解除對象與對象之間的緊耦合關系。增加一個中介者對象后,所有的相關對象都可以通過中介者對象來通信,而不是互相引用,所以當一個對象發生改變時,只需要通知中介者對象即可。
#####小結:
中介者模式是迎合迪米特法則的一種實現。迪米特法則也叫最少知識原則,是指一個對象應該盡可能少地了解另外的對象(類似不和陌生人說話)。如果對象之間的耦合性太高,一個對象發生改變之后,難免會影響到其他的對象,在中介者模式中,對象之間幾乎不知道彼此的存在,它們只通過中介者對象來互相影響對方。
#####缺點:
會新增一個中介者對象,因為對象之間交互的復雜性,轉移成了中介者對象的復雜性,使得中介者對象經常是巨大的的。中介者對象自身往往是難以維護的對象。
一般來說,如果對象之間的復雜耦合確實導致調用和維護出現了困難,而且這些耦合度隨著項目的變化呈現指數增長曲線,那我們就可以考慮使用中介者模式來重構代碼。
***
####裝飾者模式:
裝飾者模式可以動態地給某個對象添加一些額外的職責,而不會影響這個類中派生的其他對象。
~~~
var plane = {
        fire: function(){
            console.log( '發射普通子彈' );
        }
    }
    var missileDecorator = function(){
        console.log( '發射導彈' );
    }
    var atomDecorator = function(){
        console.log( '發射原子彈' );
    }
    var fire1 = plane.fire;
    plane.fire = function(){
        fire1();
        missileDecorator();
    }
    var fire2 = plane.fire;
    plane.fire = function(){
        fire2();
        atomDecorator();
    }
    plane.fire();
    // 分別輸出: 發射普通子彈、發射導彈、發射原子彈
~~~
#####裝飾者模式和代理模式:
主要區別在于它們的意圖和設計目的。
***
####狀態模式
狀態模式的關鍵是區分事物內部的狀態,事物內部的狀態的改變往往會帶來事物的行為的改變。
#####關鍵:
把事物的每種狀態都封裝成單獨的類,跟此種狀態相關的行為都封裝在類中
#####優點:
* 狀態模式定義了狀態與行為之間的關系,并將它們封裝在一個類里。通過增加新的狀態類,很容易增加新的狀態和轉換。
* 避免Context無限膨脹,狀態切換的邏輯被分布在狀態類中,也去掉了Context中原本過多的條件分支。
* 用對象代替字符串來記錄當前狀態,使得狀態的切換更加一目了然
* Context中的請求動作和狀態類中封裝的行為可以非常容易地獨立變化而互不影響

#####缺點:
會在系統中定義許多狀態類,編寫20個狀態類是一件枯燥乏味的工作,而且系統中會因此而增加不少對象。另外,由于邏輯分散在狀態類中,雖然避開了不受歡迎的條件分支語句,但也造成了邏輯分散的問題,我們無法在一個地方久看出整個狀態機的邏輯
#####性能優化點:
* 有兩種選擇來管理state對象的創建和銷毀。第一種是僅當state對象被需要時才創建并隨后銷毀,另一種是開始久創建好所有的狀態對象,并且始終不銷毀它們。第一種可以節省內存,第二種適用于狀態切換很快
* 各個Context對象可以共享一個state對象,這也是享元模式的應用場景之一。

~~~
var Light = function(){
        this.offLightState = new OffLightState( this ); // 持有狀態對象的引用
        this.weakLightState = new WeakLightState( this );
        this.strongLightState = new StrongLightState( this );
        this.superStrongLightState = new SuperStrongLightState( this );
        this.button = null;
    };

    Light.prototype.init = function(){
        var button = document.createElement( 'button' ),
        self = this;
        this.button = document.body.appendChild( button );
        this.button.innerHTML = '開關';
        this.currState = this.offLightState; // 設置默認初始狀態
        this.button.onclick = function(){ // 定義用戶的請求動作
            self.currState.buttonWasPressed();
        }
    };

    var OffLightState = function( light ){
        this.light = light;
    };

    OffLightState.prototype.buttonWasPressed = function(){
        console.log( '弱光' );
        this.light.setState( this.light.weakLightState );
    };
~~~
***
####適配器模式
適配器模式主要用來解決兩個已有接口之間不匹配的問題,它不考慮這些接口是怎樣實現的,也不考慮它們將來可能如何演化。適配器模式不需要改變已有的接口,就能把它們協同作用。
#####現實場景例子:
充電適配器
#####和其他相似模式的比較:
裝飾者模式和代理模式也不會改變原有對象的接口,但裝飾者模式的作用是為了給對象增加功能。裝飾者模式常常會刑場一條長的裝飾鏈,而適配器模式通常只包裝一次。代理模式是為了控制對對象的訪問,通常也只包裝一次。
外觀模式的作用倒是和適配器比較相似,有人把外觀模式看成一組對象的適配器,但外觀模式最顯著的特點是定義了一個新的接口。
~~~
    var googleMap = {
        show: function(){
            console.log( '開始渲染谷歌地圖' );
        }
    };
    var baiduMap = {
        display: function(){
            console.log( '開始渲染百度地圖' );
        }
    };
    var baiduMapAdapter = {
        show: function(){
            return baiduMap.display();

        }
    };

    renderMap( googleMap ); // 輸出:開始渲染谷歌地圖
    renderMap( baiduMapAdapter ); // 輸出:開始渲染百度地圖
~~~
***
####單一職責原則(SRP)
單一職責原則的職責被定義為“引起變化的原因”。如果我們有兩個動機去改寫一個方法,那么這個方法就具有兩個職責。SRP原則體現為:一個對象(方法)只做一件事情。
#####運用:
代理模式、迭代器模式、單例模式和裝飾者模式
#####何時應該分離職責:
如果隨著需求的變化,有兩個職責總是同時變化,那就不必分離他們;職責的變化軸線僅當它們確定會發生變化時才具有意義,即使兩個職責已經被耦合在一起,但它們還沒有發生改變的征兆,那么也許沒有必要主動分離它們,在代碼需要重構的時候再進行分離液不遲
#####優點:
降低了單個類或者對象的復雜度,按照職責把對象分解成更小的粒度,有助于代碼的附庸,也有利于單元測試。當一個職責需要變更的時候,不會影響到其他職責。
#####缺點:
增加編寫代碼的復雜度。當我們按照職責把對象分解成更小的粒度之后,實際上也增大了這些對象之間相互聯系的難度。
***
####最少知識量原則(LKP)
最少知識原則也叫迪米特法則
一個軟件應用應當盡可能少地與其他實體發生相互作用。這里的軟件實體是一個廣義的概念,不僅包括對象,還包括系統給、類、模塊、函數、變量等。
#####應用:
中介者模式、外觀模式(為子系統中的一組接口提供一個一致的界面,外觀模式定義了一個高層接口,這個接口使子系統更佳容易使用)
***
####開放—封閉原則(OCP)
軟件實體(類、模塊、函數)等應該是可以拓展的,但是不可修改
#####實現:
利用對象的多態、放置掛鉤、使用回調函數
#####應用:
發布-訂閱模式、模版方法模式、策略模式、代理模式、職責鏈模式
***
####接口和面向接口編程
接口是對象能響應的請求的集合
***
####代碼重構
#####提煉函數
* 避免出現超大函數
* 獨立出來的函數有助于代碼復用
* 獨立出來的函數更容易被覆寫
* 獨立出來的函數如果擁有一個良好的命名,它本身就起到了注釋的作用

#####合并重復的條件片段
#####把條件分支語句提煉成函數
#####合理使用循環
#####提前讓函數推出嵌套條件分支
#####傳遞對象參數代替過長的參數列表
#####盡量減少參數數量
#####少用三目運算符
#####合理使用鏈式調用
#####分解大型類
#####用return退出多重循環
***
感謝您耐心看完,求贊、關注,?
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 工廠模式類似于現實生活中的工廠可以產生大量相似的商品,去做同樣的事情,實現同樣的效果;這時候需要使用工廠模式。簡單...
    舟漁行舟閱讀 7,827評論 2 17
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,242評論 25 708
  • 設計模式匯總 一、基礎知識 1. 設計模式概述 定義:設計模式(Design Pattern)是一套被反復使用、多...
    MinoyJet閱讀 3,970評論 1 15
  • 不知道是什么讓我堅持到現在。也許是懶惰,也許是每一次的軟弱。 每次爭吵都會特別恨自己,恨自己一次次的忘記,容忍。都...
    在人群里來來往往閱讀 236評論 0 1
  • 今天我們去給老爺爺老奶奶上了墳,每年的大年初一去看看他們,是我們家的一個習慣,希望這個傳統能在你這里保持下去。 每...
    思想的豬閱讀 450評論 0 0