JavaScript設計模式與開發實踐筆記

taokexia發表於2019-02-20

書名: JavaScript 設計模式與開發實戰
出版社: 圖靈社群
網頁: www.ituring.com.cn/book/1632

思維導圖

基礎知識

物件導向的 JavaScript

JavaScript 沒有提供傳統的物件導向的類式繼承和對抽象類、介面的支援,而是通過原型委託的方式實現物件間的繼承。

程式語言分靜態語言、動態語言:

靜態語言優點是在編譯時就能發現型別不匹配的錯誤和明確資料型別,提高編譯速度。缺點是迫使程式設計師依照強契約來編寫程式。

動態語言優點是編寫程式碼量少,看起來簡潔,程式設計師可以把精力更多地放在業務邏輯上面。缺點是無法保證變數型別,執行期間可能發生型別錯誤。

JavaScript 是動態語言,無需進行型別檢測,可以呼叫物件的任意方法。這一切都建立在鴨子型別上,即:如果它走起路來像鴨子,叫起來像鴨子,那它就是鴨子

鴨子模型指導我們關注物件的行為,而不是物件本身,也就是關注 Has-A,而不是 Is-A。利用鴨子模式就可以實現動態型別語言一個原則"面向介面程式設計而不是面向實現程式設計"

多型 polymorphism

同一操作作用於不同物件上面,就可以產生不同的解釋和不同執行結果。

背後思想是將"做什麼"和"誰去做、怎麼做"分離開來,即將"不變的事物"與"可能改變的事物"分離開來。把不變的隔離開來,把可變部分封裝,也符合開放-封閉原則。

var makeSound = function( animal ) {
    animal.sound();
}
// 呼叫,傳入不同物件
makeSound(new Duck());
makeSound(new Chicken());
複製程式碼

使用繼承得到多型效果,是讓物件表現出多型性最常用手段。繼承包括實現繼承和介面繼承。JavaScript 變數型別在執行期是可變的,所以 JavaScript 物件多型性是與生俱來的。

封裝

封裝的目的是將資訊隱藏。封裝包括封裝資料、封裝實現、封裝型別、封裝變化。

從設計模式層面,封裝在最重要的層面是封裝變化。設計模式可劃分為

  • 建立型模式: 建立一個物件,是一種抽象行為,目的是封裝物件的行為變化
  • 結構型模式: 封裝結構之間的組合關係
  • 行為型模式: 封裝物件的行為變化

原型模式

JavaScript 是基於原型繼承的。原型模式不單是一種設計模式,還是一種程式設計泛型。

如果使用原型模式,只需要呼叫負責克隆方法,便能完成相同功能。原型模式實現關鍵,是語言本身是否提供了 clone 方法。 ECMAScript5 提供了 Object.create 方法。

在不支援 Object.create 方法瀏覽器寫法:

Object.create = Object.create || function(obj) {
    var F = function() {};
    F.prototype = obj;
    return new F();
}
複製程式碼

通過原型模式克隆出一模一樣物件,原型模式提供一種便捷方式去建立某個類的物件,克隆只是建立物件的手段。

原型繼承本質: 基於原型鏈的委託機制。

原型變成泛型至少包括以下規則:

  • 所有資料都是物件
  • 要得到一個物件,不是通過例項化類,而是找到一個物件作為原型並克隆它
  • 物件會記住它的原型
  • 如果物件無法響應某個請求,它會把這個請求委託給它自己的原型

JavaScript 中根物件是 Object.prototype 物件. Object.prototype 是一個空的物件。 JavaScript 的每個物件,都是從 Object.prototype 克隆而來。

ECMAScript5 提供 Object.getPrototypeOf 檢視物件原型

var obj = new Object();
console.log(Object.getPrototypeOf(obj) === Object.prototype); // true
複製程式碼

JavaScript 函式既可以作為普通函式被呼叫,也可以作為構造器被呼叫。通過 new 運算子來建立物件,實際操作是先克隆 Object.prototype 物件,再進行一些其他額外的操作過程。

new 運算過程:

var objectFactory = function() {
    var obj = new Object(); // 克隆一個空物件
    Constructor = [].shift.call(arguments); // 取得外部傳入構造器
    obj.__proto__ = Constructor.prototype; // 指向正確的型別
    var ret = Constructor.apply(obj, arguments); //借用外部傳入構造器給 obj 設定屬性
    return typeof ret === 'object' ? ret : obj; // 確保構造器總會返回一個物件。
}
// 使用函式
function A(name) {this.name = name;}
var a = objectFactory(A, 'tom');
複製程式碼

JavaScript 給物件提供了一個名為 __proto__ 的隱藏屬性,某個物件的 __proto__ 屬性預設會指向它的構造器的原型物件,即{Constructor}.prototype。 Chrome 和 Firefox 等向外暴露了物件的__proto__屬性。

var a = new Object();
console.log(a.__proto__ === Object.prototype); // true
複製程式碼

當前 JavaScript 引擎下,通過 Object.create 來建立物件效率不高,通常比建構函式建立物件慢。

this、call 和 apply

JavaScript的 this 總是指向一個物件,而具體指向哪個物件實在執行時基於函式的執行環境動態繫結的,而非函式被宣告時的環境。

除去不常用的 with 和 eval 情況,具體到實際應用中, this 的指向大致情況分四種:

  1. 作為物件的方法呼叫:當函式作為物件方法呼叫時, this 指向該物件

    var obj = {
        a: 1,
        getA: function() {
            alert(this === obj); // true
            alert(this.a); // 輸出: 1
        }
    }
    obj.getA();
    複製程式碼
  2. 作為普通函式呼叫:當函式不作為物件被呼叫時,this 總指向全域性物件。這個全域性物件是 window 物件.

    window.name = 'globalName';
    var getName = function() {
        return this.name;
    }
    console.log(getName()); // globalName
    複製程式碼

    ECMAScript5 嚴格模式下,this 指向 undefined

  3. 構造器呼叫: 當 new 運算子呼叫函式時,該函式總會返回一個物件, this 指向這個物件

    var MyClass = function() {
        this.name = 'sven';
    }
    var obj = new MyClass();
    alert(obj.name); // sven;
    複製程式碼

    需要注意的是,如果構造器顯式返回一個object型別的物件,那麼此次運算結果最終會返回這個物件,而不是 this:

    var MyClass = function() {
        this.name = 'sven';
        return { // 顯式返回一個物件
            name: 'name'
        }
    }
    var obj = new MyClass();
    alert(obj.name); // name;
    複製程式碼
  4. Function.prototype.callFunction.prototype.apply 呼叫: 可以動態改變傳入的 this, 函數語言程式設計的常用函式

    var obj1 = {
        name: 'sven',
        getName: function() {
            return this.name;
        }
    };
    var obj2 = {
        name: 'name'
    };
    console.log(obj1.getName()); // sven
    console.log(obj1.getName.call(obj2)); // name
    複製程式碼

丟失的 this

替代函式 document.getElementById 這個過長的函式,使用:

var getId = function(id) {
    return document.getElementById(id);
}
getId('div1');
複製程式碼

執行時,會丟擲異常,因為許多引擎的 document.getElementById 方法的內部實現中需要用到 this。 當用 getId 引用的時候, this 指向的是 window,而不是原來的 document,可以利用 apply 來修正

document.getElementById = (function(func) {
    return function() {
        return func.apply(document, arguments);
    }
})(document.getElementById);
var getId = document.getElementById;
getId('div1');
複製程式碼

call和apply用途

call 和 apply 都是非常常用的方法。作用一模一樣,區別僅在於傳入引數形式的不同。

apply 接受兩個引數, 第一個引數指定了函式體內 this 物件的指向,第二個引數為一個帶下標的集合,這個集合可以為陣列,也可以為類陣列。

call 傳入引數數量不固定,跟 apply 相同的是,第一個引數也是代表函式體內的 this 指向,從第二個引數開始往後,每個引數被依次傳入函式。

實際用途:

  1. 改變 this 指向
  2. Function.prototype.bind: 指定函式內部的 this

簡化版bind

Function.prototype.bind = function(context) {
    var self = this; // 保持原函式
    return function() {
        // 返回一個新的函式
        return self.apply(context, arguments);
        // 執行新的函式時候,會把之前傳入的context當做新函式體內的this
    }
}
複製程式碼

優化版bind: 可以預先填入一些引數

Function.prototype.bind = function() {
    var self = this; // 保持原函式
    // 需要繫結的 this 上下文
    var context = [].shift.call(arguments);
    // 剩餘引數轉換為陣列
    var args = [].slice.call(arguments);
    return function() {
        // 返回一個新的函式
        return self.apply(context, [].concat.call(args, [].slice.call(arguments)));
        // 執行新的函式時候,會把之前傳入的context當做新函式體內的this
        // 並組合兩次分別傳入的引數,作為新函式的引數
    }
}
var obj = {name: 'sven'};
var func = function(a, b, c, d) {
    alert(this.name); // sven
    alert([a, b, c, d]); // [1, 2, 3, 4]
}.bind(obj, 1, 2);
func(3, 4);
複製程式碼
  1. 借用其他物件的方法 借用建構函式,實現類似繼承的效果
var A = function(name) {this.name = name;}
var B = function() {A.apply(this, arguments);}
var b = new B('sven');
複製程式碼

函式引數列表 arguments 是一個類陣列物件,雖然它也有下標,但它並非真正的陣列,所以不能使用陣列的相關函式,這時常借用 Array.prototype 物件上的方法。

(function(){
    Array.prototype.push.call(arguments, 3);
    console.log(arguments); // [1, 2, 3]
})(1, 2);
複製程式碼

V8 引擎中 Array.prototype.push 的實現

function ArrayPush() {
    var n = TO_UINT32( this.length ); // 被 push 物件的 length
    var m = %_ArgumentsLength(); // push引數個數
    for(var i=0; i<m; i++) {
        this[i+n] = %_ArgumentsLength(i); // 複製元素
    }
    this.length = n + m; // 修正 length 屬性的值
    return this.length;
}
複製程式碼

Array.prototype.push 實際上是一個屬性複製的過程,把引數按照下標依次新增到被 push 的物件上面,順便修改了這個物件的 length 屬性。由此推斷,我們可以把任意物件傳入 Array.prototype.push

var a = {};
Array.prototype.push.call(a, 'first');
alert(a.length); // 1
alert(a[0]); // first
複製程式碼

Array.prototype.push 要滿足兩個條件

  • 物件本身可以存取屬性, 傳入 number 型別沒有效果
  • 物件的 length 屬性可讀寫, 傳入函式呼叫 length 會報錯

閉包和高階函式

閉包的形成和變數的作用域以及變數的生存週期密切相關。

變數作用域

變數作用域指的是變數的有效範圍。

當宣告一個變數沒有使用 var 的時候,變數會變為全域性變數,容易造成命名衝突。用 var 關鍵字在函式中宣告變數,這時變數為區域性變數,只能在函式內部訪問。

在 JavaScript 中,函式可以用來創造函式作用域。搜尋變數時,會隨著程式碼執行環境建立的作用域鏈往外層逐層搜尋,一直搜尋到全域性物件為止。

變數的生存週期

對於全域性變數,生存週期是永久的,除非我們主動銷燬這個全域性變數。而函式內部 var 宣告的變數,當退出函式時,這些區域性變數便失去價值,會隨著函式結束而銷燬。

var func  = function() {
    var a = 1;
    return function() {
        a++;
        console.log(a);
    }
}
var f = func();
f(); // 2
f(); // 3
複製程式碼

當退出函式後,變數 a 並沒有消失,當執行 var f = func() ,返回一個匿名函式的引用,它可以反問到 func() 被呼叫時產生的環境,而區域性變數 a 一直處在這個環境裡。既然這個區域性變數還能被訪問,就不會被銷燬。這裡產生了一個閉包結構。

閉包的應用

  1. 封裝變數: 把一些不需要暴露在全域性的變數封裝成"私有變數"
  2. 延續區域性變數的壽命: 在一些低版本瀏覽器實現傳送請求可能會丟失資料,每次請求並不都能成功傳送 HTTP 請求,原因在於區域性變數可能隨時被銷燬,而這時還沒傳送請求,造成請求丟失。可以用閉包封裝,解決問題:
var report = (function() {
    var imgs = [];
    return function(src) {
        var img = new Image();
        imgs.push(img);
        img.src = src;
    }
})
複製程式碼
  1. 利用閉包實現完整的物件導向系統
  2. 閉包實現命令模式: 命令模式意圖是把請求封裝為物件,從而分離請求的發起者和接收者之間的耦合關係。在命令執行之前,可以預先往命令模式植入接收者。閉包可以完成這個工作,把命令接受者封閉在閉包形成的環境中。
var TV = {
    open: function() {
        consoel.log('開啟電視');
    },
    close: function() {
        console.log('關閉電視');
    }
}
var createCommand = function(receiver) {
    var execute = function() {
        return receiver.open();
    }
    var undo = function() {
        return receiver.close();
    }
    return {
        execute: execute,
        undo: undo
    }
};
var setCommand = function(command) {
    document.getElementById('execute').onclick = function() {
        command.execute();
    }
    document.getElementById('undo').onclick = function() {
        command.undo();
    }
}
setCommand(createCommand(TV));
複製程式碼
  1. 閉包和記憶體管理: 使用閉包的同時比較容易形成迴圈引用,如果閉包的作用域鏈中儲存著一些 DOM 節點,這時候就有可能造成記憶體洩漏。在 IE 瀏覽器中, 由於 BOM 和 DOM 中的物件是使用 C++ 以 COM 物件的方式實現的,而 COM 物件的垃圾收集機制是引用計數策略,在基於引用計數策略的垃圾回收機制中,如果兩物件形成迴圈引用,就可能使得物件無法回收,造成記憶體洩漏。

高階函式

高階函式至少滿足以下條件:

  • 函式可以作為引數被傳遞
    1. 回撥函式: 例如 ajax 非同步請求的應用
    2. Array.prototype.sort 接收函式指定排序規則
  • 函式可以作為返回值輸出
    1. 判斷資料型別
    2. getSingle 單例模式
// 型別判斷
var isString = function(obj) {
    return Object.prototype.toString.call(obj) === '[object String]';
}
var isArray = function(obj) {
    return Object.prototype.toString.call(obj) === '[object Array]';
}
var isNumber = function(obj) {
    return Object.prototype.toString.call(obj) === '[object Number]';
}
// 使用閉包優化
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 = isNumber('Number');
// getSingle
var getSingle = function(fn) {
    var ret;
    return function() {
        return ret || (ret = fn.apply(this, arguments));
    }
}
複製程式碼

高階函式實現 AOP

AOP 面向切面程式設計: 主要作用把一些跟核心業務邏輯模組無關的功能抽離出來,這些跟業務邏輯無關的功能包括日誌統計、安全控制、異常處理等。

Java中實現AOP,通過反射和動態代理機制實現,JavaScript則是把一個函式"動態織入"另一個函式中,可以通過擴充套件 Function.prototype 實現

使用了裝飾者模式:

Function.prototype.before = function(beforefn) {
    var __self = this; // 儲存原函式的引用
    return function() {
        // 返回包含了原函式和新函式的代理函式
        beforefn.apply(this, arguments);
        return __self.apply(this,arguments);
    }
}
Function.prototype.after = function(afterfn) {
    var __self = this;
    return function() {
        var ret = __self.apply(this, arguments);
        afterfn.apply(this, arguments);
        return ret;
    }
}
var func = function() {console.log(2);}
func = func.before(function(){ 
    console.log(1);
}).after(function(){
    console.log(3);
});
func(); // 1 2 3
複製程式碼

高階函式的其他應用

  1. currying: 柯里化,又稱部分求值。一個 currying 的函式首先接受一些引數,然後返回另一個函式,剛才傳入的引數在函式形成閉包中被儲存起來。待到函式被真正需要求值的時候,之前傳入的引數一次性應用於求值。

通用 currying

var currying = function(fn) {
    var args = [];
    return function() {
        if(arguments.length === 0) {
            return fn.apply(this, args);
        } else {
            [].push.apply(args, arguments);
            return arguments.callee;
        }
    }
}
// 案例
var cost = (function() {
    var money = 0;
    return function() {
        for(var i = 0, l = arguments.length; i < l; i++) {
            money += arguments[i];
        }
        return money;
    }
});
var cost = currying(cost); // 轉化成curring函式
cost(100);
cost(200);
cost(300);
cost(); // 600
複製程式碼
  1. uncurrying: 把泛化的 this 提取出來。

實現方式之一:

Function.prototype.uncurrying = function() {
    var self = this;
    return function() {
        var obj = Array.prototype.shift.call(arguments);
        return self.apply(obj, arguments);
    }
}
// 轉化陣列的push為通用函式
var push = Array.prototype.push.uncurrying();
(function(){
    push( arguments, 4);
    console.log(arguments); // 1, 2, 3, 4
})(1, 2, 3);
複製程式碼
  1. 函式節流: 在一些場景下,函式有可能被非常頻繁地呼叫,而造成大的效能問題。例如
    • window.onresize事件,當瀏覽器視窗大小被拖動而改變時,事件觸發頻率非常高
    • mousemove事件,拖拽事件
    • 上傳進度。頻繁的進行進度通知

這些的共同問題是函式被觸發的頻率太高。

程式碼實現: throttle 函式的原理是將即將被執行的函式用 setTimeout 延遲一段時間執行。如果該次延遲執行還沒有完成,則會忽略接下來呼叫該函式的請求。 throttle 函式接受2個引數,第一個引數為需要延遲執行的函式,第二個引數為延遲執行的時間

var throttle = function(fn, interval) {
    var __self = fn; // 儲存需要被延遲執行的函式引用
    var timer; // 定時器
    var firstTime = true; // 是否是第一次呼叫
    return function() {
        var args = arguments;
        var _me = this;
        if(firstTime) {
            // 如果是第一次呼叫,不需要延遲執行
            __self.apply(__me, args);
            return firstTime = false;
        }
        if(timer) {
            // 如果定時器還在,說明前一次延遲執行還沒有完成
            return false;
        }
        timer = setTimeout(function(){
            clearTimout(timer);
            timer = null;
            __self.apply(__me, args);
        }, interval || 500);
    };
};
// 案例
window.onresize = throttle(function() {
    console.log(1);
}, 500);
複製程式碼
  1. 分時函式: 某些函式由使用者主動呼叫,但是因為一些客觀原因,會嚴重影響頁面的效能。比如在頁面中一次性渲染包含成千上百個節點的頁面,在短時間內往頁面中大量新增 DOM 節點會讓瀏覽器吃不消,造成瀏覽器卡頓甚至假死。

解決方案之一是下面的 timeChunk 函式,讓建立節點的工作分批進行。 timeChunk 函式接受3個引數,第一個引數是建立節點用到的資料,第2個引數是封裝了建立節點邏輯的函式,第3個參數列示每一批建立節點數量。

var timeChunk = function(ary, fn, count) {
    var obj;
    var t;
    var len = ary.length;
    var start = function() {
        for(var i = 0;i < Math.min(count || 1, ary.length); i++) {
            var obj = ary.shift();
            fn(obj);
        }
    };
    return function() {
        t = setInterval(function() {
            if(ary.length === 0) {
                // 如果節點都已經被建立好
                return clearInterval(t);
            }
            start();
        }, 200);
    };
};
複製程式碼
  1. 惰性載入函式: 第一次進入條件分支後,在函式內部重寫這個函式,重寫後的函式就是我們期望的函式,下一次再進入該函式就不再存在分支語句
var addEvent = function(elem, type, handler) {
    if(window.addEventListener) {
        addEvent = function(elem, type, handler) {
            elem.addEventListener(type, handler, false);
        }
    } else if(window.attachEvent) {
        addEvent = function(elem, type, handler) {
            elem.attachEvent('on'+type, handler);
        }
    }
    addEvent(elem, type, handler);
}
複製程式碼

設計模式

介紹了 JavaScript 開發中常見的 14種設計模式

單例模式

定義是: 保證一個類僅有一個例項,並提供一個訪問它的全域性訪問點。

var Singleton = function(name) {
    this.name = name;
    this.instacne = null;
}
Singleton.getInstance = (function() {
    var instance = null;
    return function(name) {
        if(!instance) {
            instance = new Singleton(name);
        }
        return instane;
    }
})
複製程式碼

可以通過結合代理模式來實現單例模式。

使用名稱空間

適當使用名稱空間,並不會杜絕全域性變數,減少全域性變數數量

var namespace = {
    a: function () {alert(1);},
    b: function () {alert(2);}
}
複製程式碼

動態建立名稱空間

var MyApp = {};
MyApp.namespace = function(name) {
    var parts = name.splice('.');
    var current = MyApp;
    for(var i in parts) {
        if(!current[parts[i]]) {
            current[parts[i]] = {};
        }
        current = current[parts[i]];
    }
}
// 案例
MyApp.namespace('dom.style');
var MyApp = {
    dom: {
        style: {}
    }
}
複製程式碼

惰性單例模式: 指需要的時候才建立物件例項。

var getSingle = function(fn) {
    var result;
    return function() {
        return result || (result = fn.apply(this, arguments));
    }
}
複製程式碼

傳入建立物件的函式,之後再讓 getSingle 返回一個新的函式,並且用一個變數 result 來儲存 fn 的計算結果。 result 變數因為身在閉包中,它永遠不會被銷燬。

這樣就把建立例項物件的職責和管理單例的職責分別放置在兩個方法裡,這兩個方法獨立變化互不影響,當他們連線在一起時,就完成了建立唯一例項物件的功能。符合單一職責原則。

不僅用於建立物件,還可用於繫結事件。

策略模式

定義是: 定義一系類的演算法,把它們一個個封裝起來,並且使它們可以互相替換。

一個基於策略模式的程式至少由兩部分組成。第一個部分是一組策略類,策略類封裝了具體的演算法,並負責具體的計算過程。第二個部分是環境類 Context,Context 接收客戶的請求,隨後請求委託給某一個策略類。 Context 中維持對某個策略物件的引用。

var strategies = {
    'S': function(salary) {
        return salary * 4;
    },
    'A': function(salary) {
        return salary * 3;
    },
    'B': function(salary) {
        return salary * 2;
    }
};
var calculateBonus = function(level, salary) {
    return strategies[level](salary);
}
// 案例
console.log(calculateBonus('S', 20000));
複製程式碼

在實際開發中,我們常把演算法含義擴散開來,使策略模式也可以封裝一系列業務規則。例如利用策略模式來進行表單驗證。

/*************** 策略物件 *******************/
var strategies = {
    isNonEmpty: function(value, errorMsg) { // 不為空
        if (value === '') {
            return errorMsg;
        }
    },
    minLength: function(value, length, errorMsg) { // 限制最小長度
        if (value.length < length) {
            return errorMsg;
        }
    },
    isMobile: function(value, errorMsg) { // 手機號碼格式
        if (!/(^1[3|5|8][0-9]{9}$)/.test(value)) {
            return errorMsg;
        }
    }
};
/****** 定義類來儲存要檢驗的內容 **********/
var Validator = function() {
    this.cache = []; // 儲存校驗規則
};
Validator.prototype.add = function(dom, rule, errorMsg) {
    var ary = rule.split(':'); // 把strategy 和引數分開
    this.cache.push(function() { // 把校驗的步驟用空函式包裝起來,並且放入cache
        var strategy = ary.shift(); // 使用者挑選的strategy
        ary.unshift(dom.value); // 把input 的value 新增進引數列表
        ary.push(errorMsg); // 把errorMsg 新增進引數列表
        return strategies[strategy].apply(dom, ary);
    });
};
Validator.prototype.start = function() {
    for (var i = 0, validatorFunc; validatorFunc = this.cache[i++];) {
        var msg = validatorFunc(); // 開始校驗,並取得校驗後的返回資訊
        if (msg) { // 如果有確切的返回值,說明校驗沒有通過
            return msg;
        }
    }
};
/********** 客戶呼叫程式碼 *******************/
var validataFunc = function() {
    var validator = new Validator(); // 建立一個validator 物件
    /***************新增一些校驗規則****************/
    validator.add(registerForm.userName, 'isNonEmpty', '使用者名稱不能為空');
    validator.add(registerForm.password, 'minLength:6', '密碼長度不能少於6 位');
    validator.add(registerForm.phoneNumber, 'isMobile', '手機號碼格式不正確');
    var errorMsg = validator.start(); // 獲得校驗結果
    return errorMsg; // 返回校驗結果
}
var registerForm = document.getElementById('registerForm');
registerForm.onsubmit = function() {
    var errorMsg = validataFunc(); // 如果errorMsg 有確切的返回值,說明未通過校驗
    if (errorMsg) {
        alert(errorMsg);
        return false; // 阻止表單提交
    }
};
複製程式碼

策略模式優點:

  1. 利用組合、委託、多型等技術和思想,可以有效地避免多重條件選擇語句
  2. 提供了對開封-封閉原則的完美支援,將演算法封裝在獨立的 strategy中,使得它們易於切換,易於理解,易於擴充套件。
  3. 演算法可以複用到系統其他地方,從而避免許多重複的複製貼上操作
  4. 利用組合和委託來讓 Context 擁有執行演算法的能力,也是繼承的一種替代方案

代理模式

代理模式是為一個物件提供一個代用品或佔位符,以便控制對它的訪問。

兩種代理模式: 通過代理拒絕某些請求的方式為保護代理,用於控制不同許可權物件對目標物件的訪問; 把一些開銷很大的物件,延遲到真正需要它的時候才建立為虛擬代理

虛擬代理案例:

var myImage = (function() {
    var imgNode = document.createElement('img');
    document.body.appendChild(imgNode);
    return {
        setSrc: function(src) {
            imgNode.src = src;
        }
    }
})();
// 代理,先顯示本地圖片,載入完成後顯示網路圖片
var proxyImage = (function() {
    var img = new Image;
    img.onload = function() {
        myImage.setSrc(this.src);
    }
    return {
        setSrc: function(src) {
            myImage.setSrc('loading.jpg');
            img.src = src;
        }
    }
})();
proxyImage.setSrc('https://p.qpic.cn/music_cover/Fe6emiag6IuVbMib3oN6yctRX8ZBICoa4liaYZkwZoSCaJdw7tOW5bCiaA/300?n=1');
複製程式碼

單一職責原則: 就一個類而言,應該僅有一個引起它變化的原因。職責被定義為"引起變化的原因"。

如果代理物件和本體物件都為一個函式,函式必然都能被執行,則可以認為他們也具有一致的“介面”。

var myImage = (function() {
    var imgNode = document.createElement('img');
    document.body.appendChild(imgNode);
    // 返回函式
    return function(src) {
        imgNode.src = src;
    }
})();
複製程式碼

虛擬代理合並 HTTP 請求

// 傳送檔案的時間,用於繫結事件,並且在點選的同時往另一臺伺服器同步檔案:
var synchronousFile = function(id) {
    console.log('開始同步檔案,id 為: ' + id);
};
// 延遲2秒,把所有請求一起傳送,減輕伺服器負擔
var proxySynchronousFile = (function() {
    var cache = [], // 儲存一段時間內需要同步的ID
        timer; // 定時器
    return function(id) {
        cache.push(id);
        if (timer) { // 保證不會覆蓋已經啟動的定時器
            return;
        }
        timer = setTimeout(function() {
            synchronousFile(cache.join(',')); // 2 秒後向本體傳送需要同步的ID 集合
            clearTimeout(timer); // 清空定時器
            timer = null;
            cache.length = 0; // 清空ID 集合
        }, 2000);
    }
})();
複製程式碼

快取代理

快取代理可以為一些開銷大的運算結果提供暫時的儲存,在下次運算時,如果傳遞進來的引數跟之前的一致,可以直接返回前面儲存的運算結果。

/**************** 計算乘積 *****************/
var mult = function(){
    var a = 1;
    for ( var i = 0, l = arguments.length; i < l; i++ ){
        a = a * arguments[i];
    }
    return a;
};
/**************** 計算加和 *****************/
var plus = function(){
    var a = 0;
    for ( var i = 0, l = arguments.length; i < l; i++ ){
        a = a + arguments[i];
    }
    return a;
};
/********* 建立快取代理的工廠 *************/
var createProxyFactory = function( fn ){
    // 建立快取物件
    var cache = {};
    return function(){
        var args = Array.prototype.join.call( arguments, ',' );
        if ( args in cache ){
            return cache[ args ];
        }
        return cache[ args ] = fn.apply( this, arguments );
    }
};
// 案例
var proxyMult = createProxyFactory( mult ),
proxyPlus = createProxyFactory( plus );
console.log( proxyMult( 1, 2, 3, 4 ) ); // 輸出:24
console.log( proxyPlus( 1, 2, 3, 4 ) ); // 輸出:10
複製程式碼

其他代理模式: 防火牆代理、遠端代理、保護代理、智慧引用代理等

迭代器模式

迭代器模式指提供一種方法順序訪問一個聚合物件中的各個元素,而又不需要暴露該物件的內部表示。如 JavaScript 中的 Array.prototype.forEach

內部迭代器: 接受2個引數,第一個為被迴圈的陣列,第二個為迴圈中的每一步後將觸發的回撥函式。內部迭代器呼叫方便,外界不用關心內部實現,跟迭代器互動僅第一次初始呼叫,缺陷是內部迭代規則無法進行修改。

var each = function(ary, callback) {
    for(var i = 0, l = ary.length; i < l; i++) {
        callback.call(ary[i], i, ary[i]); // 把下標和元素作為引數傳給callback函式
    }
}
複製程式碼

外部迭代器: 必須顯示請求迭代下一個元素。增加了呼叫複雜度,但也相對增加了靈活性。

// 適用面更廣,能滿足更多變的需求
var Iterator = function( obj ){
    var current = 0;
    var next = function(){
        current += 1;
    };
    var isDone = function(){
        return current >= obj.length;
    };
    var getCurrItem = function(){
        return obj[ current ];
    };
    return {
        next: next,
        isDone: isDone,
        getCurrItem: getCurrItem
    }
};
// 比較函式
var compare = function( iterator1, iterator2 ){
    while( !iterator1.isDone() && !iterator2.isDone() ){
        if ( iterator1.getCurrItem() !== iterator2.getCurrItem() ){
            throw new Error ( 'iterator1 和iterator2 不相等' );
        }
        iterator1.next();
        iterator2.next();
    }
    console.log( 'iterator1 和iterator2 相等' );
}
var iterator1 = Iterator( [ 1, 2, 3 ] );
var iterator2 = Iterator( [ 1, 2, 3 ] );
compare( iterator1, iterator2 ); // 輸出:iterator1 和iterator2 相等
複製程式碼

迭代器模式不僅可以迭代陣列,還可以迭代一些類陣列物件。只要被迭代的聚合物件具有 length 屬性且可以用下標訪問,就可以被迭代。

// 倒序迭代器
var reverseEach = function(ary, callback) {
    for(var l = ary.length-1; l >= 0; l--) {
        callback(l, ary[l]);
    }
}
// 終止迭代器
var each = function( ary, callback ){
    for ( var i = 0, l = ary.length; i < l; i++ ){
        if ( callback( i, ary[ i ] ) === false ){ // callback 的執行結果返回false,提前終止迭代
            break;
        }
    }
};
複製程式碼

釋出-訂閱模式

釋出訂閱模式又叫觀察者模式,它定義物件間的一種一對多的依賴關係,當一個物件的狀態發生改變時,所有依賴於它的物件都將得到通知。

釋出訂閱模式可以廣泛應用於非同步程式設計中,這是一種替代傳遞迴調函式的方案。釋出訂閱模式可以取代物件之間硬編碼的通知機制,一個物件不用再顯式地呼叫另外一個物件的某個介面。

DOM 節點上繫結事件函式就是應用了釋出訂閱模式。

通用釋出-訂閱模式程式碼:

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 ), // 獲得事件 key
        fns = this.clientList[ key ];
        if ( !fns || fns.length === 0 ){ // 如果沒有繫結對應的訊息
            return false;
        }
        for( var i = 0, fn; fn = fns[ i++ ]; ){
            fn.apply( this, arguments ); // arguments 是trigger 時帶上的引數
        }
    },
    remove: function( key, fn ){ // 移除訂閱
		var fns = this.clientList[ key ];
		if ( !fns ){ // 如果key 對應的訊息沒有被人訂閱,則直接返回
			return false;
		}
		if ( !fn ){ // 如果沒有傳入具體的回撥函式,表示需要取消key 對應訊息的所有訂閱
			fns && ( fns.length = 0 );
		}else{
			for ( var l = fns.length - 1; l >=0; l-- ){ // 反向遍歷訂閱的回撥函式列表
				var _fn = fns[ l ];
				if ( _fn === fn ){
					fns.splice( l, 1 ); // 刪除訂閱者的回撥函式
				}
			}
		}
	}
};
// 可以給所有的物件安裝釋出-訂閱功能
var installEvent = function( obj ){
    for ( var i in event ){
        obj[ i ] = event[ i ];
    }
};
複製程式碼

模組間通訊

利用釋出訂閱模式,可以在兩個封裝良好的模組中通訊,這兩個模組完全不知道對方的存在。

// 通用釋出-訂閱模式
var Event = (function() {
    var clientList = {},
        listen,
        trigger,
        remove;
    listen = function(key, fn) {
        if (!clientList[key]) {
            clientList[key] = [];
        }
        clientList[key].push(fn);
    };
    trigger = function() {
        var key = Array.prototype.shift.call(arguments),
            fns = clientList[key];
        if (!fns || fns.length === 0) {
            return false;
        }
        for (var i = 0, fn; fn = fns[i++];) {
            fn.apply(this, arguments);
        }
    };
    remove = function(key, fn) {
        var fns = clientList[key];
        if (!fns) {
            return false;
        }
        if (!fn) {
            fns && (fns.length = 0);
        } else {
            for (var l = fns.length - 1; l >= 0; l--) {
                var _fn = fns[l];
                if (_fn === fn) {
                    fns.splice(l, 1);
                }
            }
        }
    };
    return {
        listen: listen,
        trigger: trigger,
        remove: remove
    }
})();
// 模組a
var a = (function() {
    var count = 0;
    var button = document.getElementById('count');
    button.onclick = function() {
        Event.trigger('add', count++);
    }
})();
// 模組b
var b = (function() {
    var div = document.getElementById('show');
    Event.listen('add', function(count) {
        div.innerHTML = count;
    });
})();
複製程式碼

在某些情況下,還可以先把釋出的訊息儲存下來,等到有物件訂閱它的時候,再重新把訊息傳送給訂閱者。例如: QQ中離線訊息。

使用名稱空間解決命名衝突,同時新增儲存訊息的功能

// 更新Event,使得可以在訂閱之前儲存釋出的內容
var Event = (function(){
    var global = this,
    Event,
    _default = 'default';
    Event = function(){
        var _listen,
        _trigger,
        _remove,
        _slice = Array.prototype.slice, // 繫結原生Array函式
        _shift = Array.prototype.shift, // 繫結原生Array函式
        _unshift = Array.prototype.unshift, // 繫結原生Array函式
        namespaceCache = {},
        _create,
        find,
        each = function( ary, fn ){  // 迭代器
            var ret;
            for ( var i = 0, l = ary.length; i < l; i++ ){
                var n = ary[i];
                ret = fn.call( n, i, n);
            }
            return ret;
        };
        _listen = function( key, fn, cache ){   // 新增監聽
            if ( !cache[ key ] ){
                cache[ key ] = [];
            }
            cache[key].push( fn );
        };
        _remove = function( key, cache ,fn){ // 移除監聽
            if ( cache[ key ] ){
                if( fn ){
                    for( var i = cache[ key ].length; i >= 0; i-- ){
                        if( cache[ key ] === fn ){
                            cache[ key ].splice( i, 1 );
                        }
                    }
                }else{
                    cache[ key ] = [];
                }
            }
        };
        _trigger = function(){ // 觸發事件
            var cache = _shift.call(arguments),
            key = _shift.call(arguments),
            args = arguments,
            _self = this,
            ret,
            stack = cache[ key ];
            if ( !stack || !stack.length ){
                return;
            }
            return each( stack, function(){
                return this.apply( _self, args );
            });
        };
        _create = function( namespace ){ // 建立名稱空間
            var namespace = namespace || _default;
            var cache = {},
            offlineStack = [], // 離線事件
            ret = {
                listen: function( key, fn, last ){
                    _listen( key, fn, cache );
                    if ( offlineStack === null ){
                        return;
                    }
                    if ( last === 'last' ){
                    }else{
                        each( offlineStack, function(){
                            this();
                        });
                    }
                    offlineStack = null;
                },
                one: function( key, fn, last ){
                    _remove( key, cache );
                    this.listen( key, fn ,last );
                },
                remove: function( key, fn ){
                    _remove( key, cache ,fn);
                },
                trigger: function(){
                    var fn,
                    args,
                    _self = this;
                    _unshift.call( arguments, cache );
                    args = arguments;
                    fn = function(){
                        return _trigger.apply( _self, args );
                    };
                    if ( offlineStack ){
                        return offlineStack.push( fn );
                    }
                    return fn();
                }
            };
            return namespace ?
            ( namespaceCache[ namespace ] ? namespaceCache[ namespace ] :
                namespaceCache[ namespace ] = ret )
            : ret;
        };
        return {
            create: _create,
            one: function( key,fn, last ){
                var event = this.create( );
                event.one( key,fn,last );
            },
            remove: function( key,fn ){
                var event = this.create( );
                event.remove( key,fn );
            },
            listen: function( key, fn, last ){
                var event = this.create( );
                event.listen( key, fn, last );
            },
            trigger: function(){
                var event = this.create( );
                event.trigger.apply( this, arguments );
            }
        };
    }();
    return Event;
})();

/************* 先發布後訂閱 ***************/ 
Event.trigger('click', 1);
Event.listen('click', function(a) {
    console.log(a);     
});


/********** 使用名稱空間 ******************/ 
Event.create('namespace1').listen('click', function(a) {
    console.log(a);
})

Event.create('namespace1').trigger('click', 1);

Event.create('namespace2').listen('click', function(a) {
    console.log(a);
})
Event.create('namespace2').trigger('click', 2);
複製程式碼

JavaScript 無需選擇使用推模型還是拉模型。推模型指在事件發生時,釋出者一次性把所有更改的狀態和資料都推送給訂閱者。拉模型是釋出者僅僅通知訂閱者事件已經發生了,此外發布者要提供一些公開的介面供訂閱者來主動拉取資料。

釋出-訂閱模式優點是時間上解耦和物件間解耦。應用非常廣泛。缺點是建立訂閱者本身要消耗一定時間和記憶體,弱化物件之間的聯絡。

命令模式

命令模式是最簡單和優雅的模式之一,命令模式中的命令指的是一個執行某些特定事情的指令。

最常用的場景是: 有時候需要向某些物件傳送請求,但是並不知道請求的接收者是誰,也不知道被請求的操作是什麼。此時需要一種鬆耦合的方式設計程式,使得請求的傳送者和接收者能消除彼此的耦合關係。

命令模式的由來,其實是回撥(callback)函式的一個物件導向的替代品。

var button1 = document.getElementById('button1');
// 設定命令,接受按鈕和繫結的函式
var setCommand = function(button, command) {
    button.onclick = function() {
        command.execute();
    }
};
var MenuBar = {
    refresh: function() {
        alert('重新整理選單介面');
    }
};
// 設定命令,對外提供 execute 執行函式
var RefreshMenuBarCommand = function(receiver) {
    return {
        execute: function() {
            receiver.refresh();
        }
    }
};
var refreshMenuBarCommand = RefreshMenuBarCommand(MenuBar);
setCommand(button1, refreshMenuBarCommand);
複製程式碼

巨集命令

巨集命令是一組命令的集合,通過執行巨集命令的方式,可以一次執行一批命令。

var closeDoorCommand = {
    execute: function(){
        console.log( '關門' );
    }
};
var openPcCommand = {
    execute: function(){
        console.log( '開電腦' );
    }
};

var openQQCommand = {
    execute: function(){
        console.log( '登入QQ' );
    }
};

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 macroCommand = MacroCommand();
macroCommand.add( closeDoorCommand );
macroCommand.add( openPcCommand );
macroCommand.add( openQQCommand );
macroCommand.execute();
複製程式碼

傻瓜命令: 一般來說,命令模式都會在 command 物件中儲存一個接收者來負責真正執行客戶的請求,只負責把客戶的請求轉交給接收者來執行,這種模式是請求發起者和請求接收者儘可能地得到了解耦。

聰明命令: 命令物件可以直接實現請求,這樣一來就不再需要接受者的存在,這種命令物件也叫智慧命令。沒有接受者的智慧命令與策略模式相近,只能通過意圖進行分辨。策略模式所有策略物件是一致的,是達到某個目標不同手段。而智慧命令模式更廣,物件解決目標具有發散性。命令模式還可以完成撤銷、排隊等功能。

組合模式

組合模式: 用小的子物件來構建更大的物件,而這些小的子物件本身也許是由更小的"孫物件"構成的。

在巨集命令中, macroCommand 被稱為組合物件,closeDoorCommand、openPcCommand都是葉物件。

組合模式將物件組成樹形結構,以表示"部分-整體"的層次結構。除了用來表示樹形結構之外,組合模式的另一個好處是通過物件的多型性表現,使得使用者對單個物件和組合物件的使用具有一致性。

在組合模式中,客戶將統一地使用組合結構中的所有物件,無需關心它究竟是組合物件還是單個物件。

在組合模式中,請求在樹中傳遞過程總是遵循一種邏輯: 請求從樹最頂端物件向下傳遞,遇到葉物件則進行處理,遇到組合物件,則會遍歷下屬的子節點,繼續傳遞請求。

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);
/*********現在把所有的命令組合成一個“超級命令”**********/
var macroCommand = MacroCommand();
macroCommand.add(openAcCommand);
macroCommand.add(macroCommand1);
/*********最後給遙控器繫結“超級命令”**********/
var setCommand = (function(command) {
    document.getElementById('button').onclick = function() {
        command.execute();
    }
})(macroCommand);
複製程式碼

組合模式的最大優點在於可以一致地對待組合物件和基本物件。客戶不需要知道當前處理的是巨集命令還是普通命令,只要是一個命令,並且有 execute 方法,就可以加入到樹中。

安全問題

組合物件可以擁有子節點。葉物件下面沒有子節點,這時如果試圖往葉物件中新增子節點是沒有效果的。解決方案是新增 throw 處理:

var leafCommand = {
    // 子節點
    execute: function() {
        console.log('子節點執行操作');
    },
    add: function() {
        throw new Error('葉物件不能新增位元組點');
    }
}
複製程式碼

組合模式可用於檔案掃描,檔案結構是樹形的。

注意地方

  1. 組合模式不是父子關係,是一種 HAS-A(聚合)關係
  2. 對葉物件操作的一致性: 不適宜用於處理個別情況
  3. 雙向對映關係: 如果兩個父節點都包含一個相同的子節點,這種複合情況需要父節點和子節點建立雙向對映關係,但會造成複雜的引用關係,可以引入中介者模式管理
  4. 用職責鏈模式提高組合模式效能: 在組合模式中,如果樹結構比較複雜,節點數量多,在遍歷樹過程中,效能表現不理想,這時可以使用職責鏈模式,避免遍歷整顆樹。

在組合模式中,父物件和子物件之間實際上形成了天然的職責鏈。讓請求順著鏈條從父物件往子物件傳遞,或反向傳遞,直到遇到可以處理該請求物件為止。這是職責鏈的經典場景之一。

可以在子節點新增 parent 屬性記錄父節點的索引,在執行 add 操作時候更新子節點的 parent 屬性。

適用場景

  1. 表示物件的部分-整體層次結構
  2. 客戶希望統一對待樹中所有物件

模板方法模式

模板方法是一種指需要使用繼承就可以實現的簡單模式。由兩部分構成,第一部分是抽象父類,第二部分是具體的實現子類。

鉤子方法(hook) 用於隔離變化的一種手段,在父類中容易變化的地方放置鉤子,鉤子可以有一種預設的實現,由子類決定是否使用鉤子。

非使用 prototype 原型繼承的例子:

// 模板方法
var Beverage = function( param ){
    var boilWater = function(){
        console.log( '把水煮沸' );
    };
    var brew = param.brew || function(){
        throw new Error( '必須傳遞brew 方法' );
    };
    var pourInCup = param.pourInCup || function(){
        throw new Error( '必須傳遞pourInCup 方法' );
    };
    var addCondiments = param.addCondiments || function(){
        throw new Error( '必須傳遞addCondiments 方法' );
    };
    var customerWantsCondiments = param.customerWantsCondiments || function() {
        return true; // 預設需要調料
    };
    var F = function(){};
    F.prototype.init = function(){
        boilWater();
        brew();
        pourInCup();
        if (this.customerWantsCondiments()) { 
            // 如果掛鉤返回true,則需要調料
            this.addCondiments();
        }
    };
    return F;
};
var Coffee = Beverage({
    brew: function(){
        console.log( '用沸水沖泡咖啡' );
    },
    pourInCup: function(){
        console.log( '把咖啡倒進杯子' );
    },
    addCondiments: function(){
        console.log( '加糖和牛奶' );
    }
});

var Tea = Beverage({
    brew: function(){
        console.log( '用沸水浸泡茶葉' );
    },
    pourInCup: function(){
        console.log( '把茶倒進杯子' );
    },
    addCondiments: function(){
        console.log( '加檸檬' );
    }
});
var coffee = new Coffee();
coffee.init();
var tea = new Tea();
tea.init();
複製程式碼

好萊塢原則

好萊塢原則即我們允許元件將自己掛鉤到高層元件中,而高層元件決定什麼時候、以何種方式去使用這些底層元件。模板方法模式是好萊塢原則的一個典型使用場景。除此之外,好萊塢原則還常常應用於釋出-訂閱模式和回撥函式。

享元模式

享元模式是一種用於效能優化的模式,核心是運用共享技術來有效支援大量細粒度的物件。適用於大量建立相似物件的場景,減少記憶體佔用。

享元模式要求將物件的屬性劃分為內部狀態和外部狀態(狀態通常指屬性),目標是儘量減少共享物件數量,劃分狀態經驗:

  1. 內部狀態儲存於物件內部
  2. 內部狀態可以被一些物件共享
  3. 內部狀態獨立於具體的場景,通常不會改變
  4. 外部狀態取決於具體的場景,並根據場景而變化,外部狀態不能被共享

檔案上傳案例

// 剝離外部狀態
var Upload = function(uploadType) {
    this.uploadType = uploadType;// 上傳方式屬於內部狀態
};
// 取消檔案的上傳
Upload.prototype.delFile = function(id) {
    // 把當前id對應的物件的外部狀態組裝到共享物件中
    // 給共享物件設定正確的fileSize
    uploadManager.setExternalState(id, this); 
    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);
    }
};

// 建立上傳物件
// 只有兩個物件
startUpload('plugin', [{
    fileName: '1.txt',
    fileSize: 1000
}, {
    fileName: '2.html',
    fileSize: 3000
}, {
    fileName: '3.txt',
    fileSize: 5000
}]);
startUpload('flash', [{
    fileName: '4.txt',
    fileSize: 1000
}, {
    fileName: '5.html',
    fileSize: 3000
}, {
    fileName: '6.txt',

    fileSize: 5000
}]);
複製程式碼

適用性

使用享元模式後需要分別多維護一個 factory 物件和一個 manager 物件,也帶來了一些複雜性的問題。享元模式帶來的好處很大程度上取決於如何使用及何時使用。使用場景

  • 一個程式使用大量相似物件
  • 由於使用大量物件,造成很大記憶體開銷
  • 物件多數狀態都可以變為外部狀態
  • 剝離出物件外部狀態之後,可以用相對較少的共享物件取代大量物件。

物件池

物件池維護一個裝載空閒物件的池子,如果需要物件,不是直接new,而是轉從物件池中獲取。如果物件池沒有空閒物件,再建立一個新物件。常用有 HTTP 連線池和資料庫連線池

// 物件池跟享元模式的思想相似
// 但沒有分離內部狀態和外部狀態的過程
var objectPoolFactory = function(createObjFn) {
    var objectPool = [];
    return {
        create: function() {
            // 判斷物件池是否為空
            var obj = objectPool.length === 0 ?
                createObjFn.apply(this, arguments) : objectPool.shift();
            return obj;
        },
        recover: function(obj) {
            objectPool.push(obj);// 物件池回收dom
        }
    }
};

var iframeFactory = objectPoolFactory(function() {
    var iframe = document.createElement('iframe');
    document.body.appendChild(iframe);
    iframe.onload = function() {
        iframe.onload = null; // 防止iframe 重複載入的bug
        iframeFactory.recover(iframe); // iframe 載入完成之後回收節點
    }
    return iframe;
});

var iframe1 = iframeFactory.create();
iframe1.src = 'http://baidu.com';
var iframe2 = iframeFactory.create();
iframe2.src = 'http://QQ.com';
setTimeout(function() {
    var iframe3 = iframeFactory.create();
    iframe3.src = 'http://163.com';
}, 3000);
複製程式碼

物件池是另外一種效能優化方案,它跟享元模式有些相似之處,但沒有分離內部狀態和外部狀態這個過程。

職責鏈模式

定義: 使多個物件都有機會處理請求,從而避免請求的傳送者和接收者之間的耦合關係,將這些物件連成一條鏈,並沿著這條鏈傳遞該請求,直到有一個物件處理它為止。

// 職責鏈節點函式
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( '手機庫存不足' );
    }
};
// 包裝函式的職責鏈節點
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;
};
// 非同步職責鏈,手動傳遞請求給職責鏈下個節點
Chain.prototype.next = function() {
    return this.successor && this.successor.passRequest.apply(this.successor, arguments);
}

// 案例:
var chainOrder500 = new Chain( order500 );
var chainOrder200 = new Chain( order200 );
var chainOrderNormal = new Chain( orderNormal );
// 設定下一個節點
chainOrder500.setNextSuccessor( chainOrder200 );
chainOrder200.setNextSuccessor( chainOrderNormal );
chainOrder500.passRequest( 1, true, 500 ); // 輸出:500 元定金預購,得到100 優惠券
chainOrder500.passRequest( 2, true, 500 ); // 輸出:200 元定金預購,得到50 優惠券
複製程式碼

職責鏈模式的最大優點是解耦了請求傳送者和N個接受者之間的複雜關係。除此之外,還可以手動指定起始節點,請求並不是非得從鏈中第一個節點開始傳遞。

缺陷是不能保證某個請求一定會被鏈中節點處理,可以在鏈尾增加一個保底接收者來處理這些請求。從效能方面考慮,要避免過長的職責鏈。

用 AOP 實現職責鏈

Function.prototype.after = function( fn ){
    var self = this;
    return function(){
        var ret = self.apply( this, arguments );
        if ( ret === 'nextSuccessor' ){
            return fn.apply( this, arguments );
        }
        return ret;
    }
};

var order = order500.after( order200 ).after( orderNormal );
order( 1, true, 500 ); // 輸出:500 元定金預購,得到100 優惠券
order( 2, true, 500 ); // 輸出:200 元定金預購,得到50 優惠券
複製程式碼

用 AOP 實現職責鏈簡單、巧妙,同時疊加了函式的作用域,如果鏈條過長,仍會影響效能。

中介者模式

中介者模式作用是解除物件與物件之間的緊耦合關係。增加一箇中介者物件後,所有相關物件都通過中介者物件通訊,而不是相互引用,當物件發生改變時,通知中介者物件即可。

中介者物件使得網狀的多對多關係變成一對多關係。實現中介者物件的兩種方式:

  1. 利用釋出-訂閱模式,中介者物件為訂閱者,各個物件為釋出者,一旦物件狀態改變,傳送訊息給中介者物件。
  2. 向外開放接收訊息介面,通過往介面傳遞引數來給中介者物件傳送訊息。

中介者模式應用於遊戲案例:

// 定義 玩家類
function Player(name, teamColor){
    this.name = name; // 角色名字
    this.teamColor = teamColor; // 隊伍顏色
    this.state = 'alive'; // 玩家生存狀態
};
// 獲勝
Player.prototype.win = function(){
    console.log(this.name + ' won ');
};
// 失敗
Player.prototype.lose = function(){
    console.log(this.name +' lost');
};
// 死亡
Player.prototype.die = function(){
    this.state = 'dead';
    playerDirector.reciveMessage('playerDead', this); // 給中介者傳送訊息,玩家死亡
};
// 移除
Player.prototype.remove = function(){
    playerDirector.reciveMessage('removePlayer', this); // 給中介者傳送訊息,移除一個玩家
};
// 玩家換隊
Player.prototype.changeTeam = function(color){
    playerDirector.reciveMessage('changeTeam', this, color); // 給中介者傳送訊息,玩家換隊
};
/*************** 生成玩家的工廠 ***************/
var playerFactory = function(name, teamColor) {
    var newPlayer  = new Player(name, teamColor);
    playerDirector.reciveMessage('addPlayer', newPlayer);
    return newPlayer;
}
/*************** 中介者物件 ******************/
var playerDirector= (function(){
    var players = {}; // 儲存所有玩家
    var operations = {}; // 中介者可以執行的操作
    /******** 新增一個玩家 ****************/
    operations.addPlayer = function(player){
        var teamColor = player.teamColor; // 玩家的隊伍顏色
        players[teamColor] = players[teamColor] || []; // 如果該顏色的玩家還沒有成立隊伍,則
        players[teamColor].push(player); // 新增玩家進隊伍
    };
    /******** 移除一個玩家 ******************/
    operations.removePlayer = function(player){
        var teamColor = player.teamColor, // 玩家的隊伍顏色
        teamPlayers = players[teamColor] || []; // 該隊伍所有成員
        for ( var i = teamPlayers.length - 1; i >= 0; i-- ){ // 遍歷刪除
            if (teamPlayers[i] === player){
                teamPlayers.splice(i, 1);
            }
        }
    };
    /******** 玩家換隊 *****************/
    operations.changeTeam = function(player, newTeamColor){ // 玩家換隊
        operations.removePlayer(player); // 從原隊伍中刪除
        player.teamColor = newTeamColor; // 改變隊伍顏色
        operations.addPlayer(player); // 增加到新隊伍中
    };
    /******** 玩家死亡 *****************/
    operations.playerDead = function(player){ // 玩家死亡
        var teamColor = player.teamColor,
        teamPlayers = players[teamColor]; // 玩家所在隊伍
        var all_dead = true;
        for (var i = 0, player; player = teamPlayers[ i++ ];){
            if (player.state !== 'dead'){
                all_dead = false;
                break;
            }
        }
        if (all_dead === true){ // 全部死亡
            for (var i = 0, player; player = teamPlayers[i++];){
                player.lose(); // 本隊所有玩家lose
            }
            for (var color in players){
                if (color !== teamColor){
                    var teamPlayers = players[color]; // 其他隊伍的玩家
                    for (var i = 0, player; player = teamPlayers[i++];){
                        player.win(); // 其他隊伍所有玩家win
                    }
                }
            }
        }
    };
    /******** 對外暴露介面 *****************/
    var reciveMessage = function(){
        var message = Array.prototype.shift.call(arguments); // arguments 的第一個引數為訊息名稱
        operations[message].apply(this, arguments);
    };
    return {
        reciveMessage: reciveMessage
    }
})();
// 案例
// 紅隊:
var player1 = playerFactory( '皮蛋', 'red' ),
player2 = playerFactory( '小乖', 'red' ),
player3 = playerFactory( '寶寶', 'red' ),
player4 = playerFactory( '小強', 'red' );
// 藍隊:
var player5 = playerFactory( '黑妞', 'blue' ),
player6 = playerFactory( '蔥頭', 'blue' ),
player7 = playerFactory( '胖墩', 'blue' ),
player8 = playerFactory( '海盜', 'blue' );
player1.die();
player2.die();
player3.die();
player4.die();
複製程式碼

中介者模式是迎合迪米特法則的一種實現。迪米特法則也叫最少知識原則,指一個物件應該儘可能少地瞭解另外的物件。減少耦合性。中介者模式的缺點是系統中會新增一箇中介者物件,且中介者物件複雜又巨大,是一個難以維護的物件。

裝飾者模式

裝飾者模式: 給物件動態增加職責。能夠在不改變物件自身的基礎上,在程式執行期間給物件動態地新增職責。

裝飾函式: 為函式新增新功能,可以通過儲存函式的引用,在不違反開放-封閉的前提下,給函式增加新功能

var a = function() {console.log(1);}
var _a = a;
a = function() {_a(); console.log(2);}
a();
複製程式碼

在沒有修改原來函式的基礎上給函式增加新的功能,缺陷是: 必須再維護一箇中間變數; 可能遇到 this 被劫持的問題。

用 AOP 裝飾函式

Function.prototype.before = function( beforefn ){
    var __self = this; // 儲存原函式的引用
    return function(){ // 返回包含了原函式和新函式的"代理"函式
        beforefn.apply( this, arguments ); // 執行新函式,且保證this 不被劫持,新函式接受的引數
        // 也會被原封不動地傳入原函式,新函式在原函式之前執行
        return __self.apply( this, arguments ); // 執行原函式並返回原函式的執行結果,
        // 並且保證this 不被劫持
    }
}

Function.prototype.after = function( afterfn ){
    var __self = this;
    return function(){
        var ret = __self.apply( this, arguments );
        afterfn.apply( this, arguments );
        return ret;
    }
};
複製程式碼

表單驗證案例:

var username = document.getElementById('username');
var password = document.getElementById('password');
var submitBtn = document.getElementById('submitBtn');
// 裝飾者模式
Function.prototype.before = function(beforefn) {
    var __self = this;
    return function() {
        if (beforefn.apply(this, arguments) === false) {
            // beforefn 返回false 的情況直接return,不再執行後面的原函式
            return;
        }
        return __self.apply(this, arguments);
    }
}
// 檢驗函式
var validata = function() {
    if (username.value === '') {
        alert('使用者名稱不能為空');
        return false;
    }
    if (password.value === '') {
        alert('密碼不能為空');
        return false;
    }
}
// 傳送請求
var formSubmit = function() {
    var param = {
        username: username.value,
        password: password.value
    }
    ajax('http:// xxx.com/login', param);
}
// 在傳送請求前進行標籤驗證
formSubmit = formSubmit.before(validata);
submitBtn.onclick = function() {
    formSubmit();
}
複製程式碼

裝飾者模式和代理模式

裝飾者模式和代理模式結構類似,兩種模式都描述了怎麼為物件提供一定程度上的簡介引用,它們實現部分都保留了對另外一個物件的引用,並向這個物件傳送請求。

最重要的區別在於它們的意圖和設計目的。代理模式目的是當直接訪問本體不方便或者不符合需要時,為這個本體提供一個替代者,通常只有一層代理-本體的引用,強調了代理和本體的關係。裝飾者模式作用是為物件動態加入行為,經常會形成一條長長的裝飾鏈。

狀態模式

狀態模式定義是: 允許一個物件在其內部狀態改變的時候改變它的行為,物件看起來似乎改變了它的類。其中,關鍵是區分事物內部的狀態,事物內部狀態的改變往往會帶來事物的行為改變。

狀態模式把事物的每種狀態都封裝成單獨的類,跟此種狀態有關的行為都被封裝到這個類的內部。

簡單案例:

// 狀態轉換 a -> b -> c -> a -> ...
var Astate = function(item) {
    this.item = item;
}
Astate.prototype.change = function() {
    // 改變狀態
    console.log('b');
    this.item.setState(this.item.Bstate); // 切換到 B 狀態
}
var Bstate = function(item) {
    this.item = item;
}
Bstate.prototype.change = function() {
    // 改變狀態
    console.log('c');
    this.item.setState(this.item.Cstate); // 切換到 C 狀態
}
var Cstate = function(item) {
    this.item = item;
}
Cstate.prototype.change = function() {
    // 改變狀態
    console.log('a');
    this.item.setState(this.item.Astate); // 切換到 B 狀態
}
/*************** item 類 ****************/
var Item = function() {
    this.Astate = new Astate(this);
    this.Bstate = new Bstate(this);
    this.Cstate = new Cstate(this);
    this.currState = this.Astate;
}
// 觸發改變狀態事件
Item.prototype.change = function() {
    this.currState.change();
}
// 更新狀態
Item.prototype.setState = function(newState) {
    this.currState = newState;
}
/*************** 案例 *******************/
var item = new Item();
item.change();
複製程式碼

定義抽象類

狀態類中將定義一些共同的方法, Context 最終會將請求委託給狀態物件的這些方法裡。

// 抽象類
var State = function() {}
State.prototype.change = function() {
    throw new Error('父類的 change 方法必須被重寫');
}
// 讓子類繼承
var Astate = function(item) {this.item = item;}
Astate.prototype = new State(); // 繼承抽象父類
Astate.prototype.change = function() {
    // 重寫父類方法
    console.log('a');
    this.item.setState(this.item.Bstate);
}
複製程式碼

檔案上傳程式

// 上傳外掛
var plugin = (function() {
    var plugin = document.createElement('embed');
    plugin.style.display = 'none';
    plugin.type = 'application/txftn-webkit';
    plugin.sign = function() {
        console.log('開始檔案掃描');
    }
    plugin.pause = function() {
        console.log('暫停檔案上傳');
    };
    plugin.uploading = function() {
        console.log('開始檔案上傳');
    };
    plugin.del = function() {
        console.log('刪除檔案上傳');
    }
    plugin.done = function() {
        console.log('檔案上傳完成');
    }
    document.body.appendChild(plugin);
    return plugin;
})();
// 建構函式,為每種狀態子類都建立一個例項物件
var Upload = function(fileName) {
    this.plugin = plugin;
    this.fileName = fileName;
    this.button1 = null;
    this.button2 = null;
    this.signState = new SignState(this); // 設定初始狀態為waiting
    this.uploadingState = new UploadingState(this);
    this.pauseState = new PauseState(this);
    this.doneState = new DoneState(this);
    this.errorState = new ErrorState(this);
    this.currState = this.signState; // 設定當前狀態
};
// 初始化上傳的 DOM 節點,並開始繫結按鈕事件
Upload.prototype.init = function() {
    var that = this;
    this.dom = document.createElement('div');
    this.dom.innerHTML =
        '<span>檔名稱:' + this.fileName + '</span>\
<button data-action="button1">掃描中</button>\
<button data-action="button2">刪除</button>';
    document.body.appendChild(this.dom);
    this.button1 = this.dom.querySelector('[data-action="button1"]');
    this.button2 = this.dom.querySelector('[data-action="button2"]');
    this.bindEvent();
};
// 具體按鈕事件的實現
Upload.prototype.bindEvent = function() {
    var self = this;
    this.button1.onclick = function() {
        self.currState.clickHandler1();
    }
    this.button2.onclick = function() {
        self.currState.clickHandler2();
    }
};
// 
Upload.prototype.sign = function() {
    this.plugin.sign();
    this.currState = this.signState;
};
Upload.prototype.uploading = function() {
    this.button1.innerHTML = '正在上傳,點選暫停';
    this.plugin.uploading();
    this.currState = this.uploadingState;
};
Upload.prototype.pause = function() {

    this.button1.innerHTML = '已暫停,點選繼續上傳';
    this.plugin.pause();
    this.currState = this.pauseState;
};
Upload.prototype.done = function() {
    this.button1.innerHTML = '上傳完成';
    this.plugin.done();
    this.currState = this.doneState;
};
Upload.prototype.error = function() {
    this.button1.innerHTML = '上傳失敗';
    this.currState = this.errorState;
};
Upload.prototype.del = function() {
    this.plugin.del();
    this.dom.parentNode.removeChild(this.dom);
};
// 實現各種狀態類,使用狀態工廠
var StateFactory = (function() {
    var State = function() {};
    State.prototype.clickHandler1 = function() {
        throw new Error('子類必須重寫父類的clickHandler1 方法');
    }
    State.prototype.clickHandler2 = function() {
        throw new Error('子類必須重寫父類的clickHandler2 方法');
    }
    return function(param) {
        var F = function(uploadObj) {
            this.uploadObj = uploadObj;
        };
        F.prototype = new State();
        for (var i in param) {
            F.prototype[i] = param[i];
        }
        return F;
    }
})();

var SignState = StateFactory({
    clickHandler1: function() {
        console.log('掃描中,點選無效...');
    },
    clickHandler2: function() {
        console.log('檔案正在上傳中,不能刪除');
    }
});
var UploadingState = StateFactory({
    clickHandler1: function() {
        this.uploadObj.pause();
    },
    clickHandler2: function() {
        console.log('檔案正在上傳中,不能刪除');
    }
});
var PauseState = StateFactory({
    clickHandler1: function() {
        this.uploadObj.uploading();
    },
    clickHandler2: function() {
        this.uploadObj.del();
    }
});
var DoneState = StateFactory({
    clickHandler1: function() {
        console.log('檔案已完成上傳, 點選無效');
    },
    clickHandler2: function() {
        this.uploadObj.del();
    }
});
var ErrorState = StateFactory({
    clickHandler1: function() {
        console.log('檔案上傳失敗, 點選無效');
    },
    clickHandler2: function() {
        this.uploadObj.del();
    }
});

var uploadObj = new Upload('JavaScript 設計模式與開發實踐');
uploadObj.init();
window.external.upload = function(state) {
    uploadObj[state]();
};
window.external.upload('sign');
setTimeout(function() {
    window.external.upload('uploading'); // 1 秒後開始上傳
}, 1000);
setTimeout(function() {
    window.external.upload('done'); // 5 秒後上傳完成
}, 5000);
複製程式碼

狀態模式優點:

  • 定義了狀態與行為之間的關係,將它們封裝在一個類裡。通過增加新的狀態類,增加新的狀態和轉換。
  • 避免 Context 無限膨脹,狀態切換的邏輯被分佈在狀態類中
  • 用物件替代字串來記錄當前狀態,使得狀態切換一目瞭然。
  • Context 中的請求動作和狀態類中封裝的行為可以容易地獨立變化互不影響。

缺點是定義了許多狀態類,不易於維護。

效能優化點:

  1. 管理 state 物件的建立和銷燬:
    1. 僅當 state 物件被需要時才建立並隨後銷燬。適用於物件較龐大的情況
    2. 一開始就建立好所有狀態物件,並且始終不銷燬。使用與狀態改變頻繁的情況
  2. 利用享元模式,每個 Context 物件都共享一個 state 物件

和策略模式關係

狀態模式和策略模式都封裝了一系類的演算法或行為,相同點是,它們都有一個上下文、一些策略或者狀態,上下文把請求委託給這些類執行。

區別是策略模式中各個策略類之間是平等又平行的,沒有任何聯絡;狀態模式中,狀態和狀態對應行為時早已封裝好的,狀態之間切換也早規定完成,改變行為這件事發生在狀態內部。

介面卡模式

介面卡模式作用是解決兩個軟體實體間的介面不相容的問題。

var EventA = {
    A: function() {
        console.log('說英語');
    }
}
var EventB = {
    B: function() {
        console.log('說中文');
    }
}
// B 的介面卡
var Badapter = {
    A: function() {
        return EventB.B();
    }
}
// 呼叫交流的函式
var communicate = function(item) {
    if(item.A instanceof Function) {
        item.A();
    }
}
communicate(A);
communicate(Badapter);
複製程式碼

介面卡模式還可以用於資料格式轉換。有一些模式跟介面卡模式的結構非常相似,比如裝飾者模式,代理模式和外觀模式。這幾種模式都屬於包裝模式,都是由一個物件來包裝另一個物件。區別它們的關鍵仍是模式的意圖。

  • 裝飾者模式主要用來解決兩個已有介面之間不匹配的問題。不需要改變已有的介面,就能使它們協同作用
  • 裝飾者模式和代理模式也不會改變原有物件介面,但裝飾者模式的作用是為了給物件增加功能,常常有一條常常的裝飾鏈;而介面卡模式只包裝一次,代理模式為了控制對物件的訪問,也只包裝一次
  • 外觀模式和介面卡相似,外觀模式的顯著特點是定義了一個新的介面。

設計原則和程式設計技巧

設計原則通常指的是單一職責原則、里氏替換原則、依賴倒置原則、介面隔離原則、合成複用原則和最少知識原則。

單一職責原則

單一職責原則(SPR)體現為: 一個物件(方法)只做一件事情。

  1. 代理模式: 虛擬代理把預載入的職責放到代理物件中
  2. 迭代器模式: 提供聚合訪問物件的方法
  3. 單例模式: 只負責建立物件
  4. 裝飾者模式: 動態給物件增加職責,是一種分離職責的方式

何時分離職責

  1. 如果隨著需求的變化,有兩個職責總是同時變化,那就不必分離他們。
  2. 職責的變化軸線僅當它們確定會發生變化時才具有意義,即使兩個職責已經耦合在一起,但它們沒有發生改變的徵兆,就沒必要分離它們,在需要重構時再分離

SPR 原則優缺點:

優點是降低了單個類或者物件的複雜度,按照職責把物件分解更小的粒度,這有助於程式碼的複用和單元測試。

缺點是會明顯增加編寫程式碼的複雜度,把物件分解成更小的粒度之後,實際上也增大了這些物件之間相互聯絡的難度。

最少知識原則

最少知識原則(LKP),也叫迪米特法則(Law of Demeter, LoD)說的是一個軟體實體應當儘可能少地與其他實體發生相互作用。

最少知識原則要求我們設計程式時,應當儘量減少物件之間的互動。如果兩個物件之間不必彼此直接同學,那麼這兩個物件就不要發生直接的相互聯絡。常見做法是引入一個第三者物件,承擔這些物件之間的通訊作用。

  1. 中介者模式: 通過增加一箇中介物件,讓所有相關物件都通過中介者物件來通訊,而不是相互引用。

  2. 外觀模式: 為子系統中的一組介面提供一個一致的介面,外觀模式定義了一個高層介面,這個介面使子系統更加容易使用。

    外觀模式的作用是對客戶遮蔽一組子系統的複雜性。

    var A = function() {
        a1();
        a2();
    }
    var B = function() {
        b1();
        b2();
    }
    var facade = function() {
        A();
        B();
    }
    facade();
    複製程式碼

    外觀模式容易跟普通封裝混淆。這兩者都封裝了一些事物,但外觀模式關鍵是定義一個高層介面去封裝一組"子系統"。外觀模式作用主要有兩點:

    1. 為一組子系統提供一個簡單便利的訪問入口
    2. 隔離客戶與複雜子系統之間的聯絡,客戶不用去了解子系統的細節。

封裝在最少知識中體現

封裝很大程度上表達的是對資料的隱藏,包括用來限制變數的作用域。

JavaScript 變數作用域規定:

  • 變數在全域性宣告,或者在程式碼的任何位置隱式宣告(不用 var),則該變數全域性可見;
  • 變數在函式內顯式什麼(用 var), 則在函式內可見

把變數可見性限制在一個儘可能小的範圍內,這個變數對其他模組影響越小,變數被改寫和發生衝突的機會也越小。

開放-封閉原則

軟體實體(類、模組、函式)等應該是可以擴充套件的,但是不可修改的

擴充套件函式功能,有兩種方式,一種是修改原有程式碼,一種是增加一段新的程式碼。

開發-封閉原則思想: 當需要改變一個程式的功能或者給這個程式增加新功能的時候,可以使用增加程式碼的方式,但不允許改動程式原始碼。

過多的條件分支語句是造成程式違反開發-封閉原則的一個常見原因。利用多型可以解決這個問題,把不變的部分隔離出來,把可變的部分封裝起來。

除了多型,還有

  1. 放置掛鉤(hook)是分離變化的一種方法。在程式可能發生變化的地方掛置一個鉤掛,掛鉤返回結果決定程式走向。
  2. 使用回撥函式

設計模式中的開發-封閉原則

  1. 釋出-訂閱模式
  2. 模板方法模式: 側重於繼承
  3. 策略模式: 側重於組合和委託
  4. 代理模式
  5. 職責鏈模式

開放-封閉原則相對性:程式完全封閉是不容易做到的。太多抽象也會增加程式複雜度。可以先做到以下兩點:

  1. 挑出最容易發生變化的地方,然後構造抽象來封閉這些變化
  2. 在不可避免發生修改的時候,儘量修改那些相對容易修改的地方。拿一個開源庫來說,修改它提供的配置檔案,總比修改它的原始碼來得簡單。

介面和麵向介面程式設計

介面是物件能夠響應的請求的集合。

抽象類和 interface 的主要作用是

  1. 通過向上轉型來隱藏物件的真正型別,以表現物件的多型性
  2. 約定類與類之間的一些契約行為。

JavaScript 不需要向上轉型,介面在 JavaScript 中最大的作用就退化到了檢查程式碼的規範性。

用鴨子型別進行介面檢查

鴨子型別時動態型別語言物件導向設計中的一個重要概念。利用鴨子型別思想,不必藉助超型別的幫助,就能在動態型別語言中輕鬆地實現面向介面程式設計,而不是面向實現程式設計。

利用鴨子型別思想判斷一個物件是否是陣列

var isArray = function(obj) {
    return obj &&
        typeof obj === 'object' &&
        typeof obj.length === 'number' &&
        typeof obj.splice === 'function'
}
複製程式碼

可以使用 TypeScript 編寫基於 interface 的命令模式

程式碼重構

模式和重構之間有著一種與生俱來的關係。設計模式的目的就是為許多重構行為提供目標。

  • 提煉函式: 重構函式的好處:

    1. 避免出現超大函式
    2. 獨立出來的函式有助於程式碼複用
    3. 獨立出來的函式更容易被複寫
    4. 獨立出來的函式如果有一個良好的命名,本身就祈禱註釋作用
  • 合併重複的條件片段

  • 把條件分支語句提煉成函式: 複雜的條件分支語句導致程式難以閱讀和理解。可以把複雜條件語句提煉成一個單獨的函式,能更準確表達程式碼意思,函式名本身又起到註釋作用。

  • 合理使用迴圈

    var createXHR = function() {
        var versions = ['MSXML2.XMLHttp.6.Oddd', 'MSXML2.XMLHttp.3.0', 'MSXML2.XMLHttp'];
        for(var i = 0, version; version = versions[i++];) {
            try{
                return new ActiveXObject(version);
            } catch(e) {
    
            }
        }
    };
    var xhr = createXHR();
    複製程式碼
  • 提前讓函式退出代替巢狀條件分支

  • 傳遞物件引數代替過長的引數列表:如果引數過多,函式就難以理解,還要記住引數的傳遞順序,可以把引數都放入一個物件內,便於維護。

  • 儘量減少引數數量

  • 少用三目運算子

  • 合理使用鏈式呼叫:鏈式應用結構相對穩定,後期不易發生修改。如果結構容易發生變化,則建議使用普通呼叫方式

    var User = {
        id: null,
        name: null,
        setId: function(id) {
            this.id = id;
            return this;
        },
        setName: function(name) {
            this.name = name;
            return this;
        }
    };
    console.log(User.setId(1314).setName('sven'));
    複製程式碼
  • 分解大型類: 拆分大型的類,使類精簡,便於理解

  • 用 return 退出多重迴圈

相關文章