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

任乃千發表於2017-04-14

《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 );
         }
    };複製程式碼
  3. 借用其他物件的方法:借用建構函式、對類陣列甚至物件(物件本身要可以存取屬性、length屬性可讀寫)使用陣列的方法

閉包和高階函式

js是一門完整的物件導向的程式語言,同時也擁有許多函式式語言的特性。

變數的作用域:

變數的有效範圍,在函式宣告變數時沒有帶關鍵字var就會變成全域性變數,使用了var時稱為區域性變數,只有在該函式內部才能訪問到這個變數,在函式外面時訪問不到的。js中函式可以用來創造函式作用域。在函式裡面可以看到外面的變數,而在函式外面無法看到函式裡面的變數,這是因為在函式中搜尋一個變數的時候,如果該函式內並沒有宣告這個變數,那麼搜尋的過程會隨著程式碼執行環境建立的作用域鏈往外層逐層搜尋,一直搜尋到全域性物件為止。

變數的生存週期:

全域性變數的生存週期是永久的,除非主動銷燬。在函式內使用var宣告的區域性變數,在函式退出時,這些區域性變數記失去了它們的價值,會隨著函式呼叫的結束而被銷燬。

閉包的作用:

因為對外部作用域的引用可以阻止外部的作用域被銷燬,延長了區域性變數的生命週期、可以把每次迴圈中的i值都封閉起來,使迴圈繫結事件符合我們的預期

閉包的更多作用:

封裝變數:

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

閉包和麵向物件設計:

物件以方法的形式包括了過程,閉包在過程中以環境的形式包含了資料。通常用物件導向思想能實現的功能,用閉包也能實現,反之亦然。

用閉包實現命令模式:

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

閉包與記憶體管理:

解決物件間迴圈引用帶來的記憶體洩漏問題,只需要把迴圈引用中的變數設為null即可。將變數設定為null意味著切斷變數與它此前引用的值之間的連線。當垃圾收集器瑕疵執行時,就會刪除這些值並回收它們佔用的記憶體

高階函式:

定義:

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

  • 函式可以作為引數被傳遞
  • 函式可以作為返回值輸出

函式作為引數傳遞:

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

  1. 回撥函式
    非同步請求、當一個函式不適合執行一些請求時,可以把這些請求封裝成一個函式,並把它作為引數傳遞給另一個函式,“委託”給另外一個函式來執行。
  2. Array.prototype.sort
    Array.prototype.sort接受一個函式當作引數,這個函式裡封裝了陣列元素的排序規則。從Array.prototype.sort的使用可以看到,我們的目的是對陣列進行排序,這是不變的部分;從而使用什麼規則去排序,則是可變的部分。把可變的部分封裝在函式引數裡,動態傳入,使這個方法稱為了一個非常靈活的方法。

函式作為返回值輸出:

  1. 判斷資料的型別
    var isType = functiontype{
     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 );複製程式碼
  4. 分時函式
    使用函式、定時器讓一個大任務分成多個小任務
  5. 惰性載入函式
    在第一次進入條件分支之後,在函式內部會重寫這個函式,重寫之後的函式就是符合當前瀏覽器環境的函式。

單例模式

實現一些只需要一個的物件,比如執行緒池、全域性快取、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退出多重迴圈


感謝您耐心看完,求贊、關注,☺

相關文章