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

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

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


面向對象的js

靜態類型:編譯時便已肯定變量的類型

優勢:

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

缺點:

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

動態類型:程序運行的時候,變量被賦予某個值以後,纔會具備某種類型

優勢:

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

缺點:

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

鴨子類型:

「若是它走起路來像鴨子,叫起來也像鴨子,那麼它就是鴨子。」程序員

鴨子類型指導咱們只關注對象的行爲,而不關注對象自己,即灌輸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退出多重循環


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

相關文章
相關標籤/搜索