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

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

思惟導圖

基礎知識

面向對象的 JavaScript

JavaScript 沒有提供傳統的面向對象的類式繼承和對抽象類、接口的支持,而是經過原型委託的方式實現對象間的繼承。html

編程語言分靜態語言、動態語言:java

靜態語言優勢是在編譯時就能發現類型不匹配的錯誤和明確數據類型,提升編譯速度。缺點是迫使程序員依照強契約來編寫程序。程序員

動態語言優勢是編寫代碼量少,看起來簡潔,程序員能夠把精力更多地放在業務邏輯上面。缺點是沒法保證變量類型,運行期間可能發生類型錯誤。web

JavaScript 是動態語言,無需進行類型檢測,能夠調用對象的任意方法。這一切都創建在鴨子類型上,即:若是它走起路來像鴨子,叫起來像鴨子,那它就是鴨子ajax

鴨子模型指導咱們關注對象的行爲,而不是對象自己,也就是關注 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 退出多重循環

相關文章
相關標籤/搜索