JavaScript中常見的十五種設計模式

在程序設計中有不少實用的設計模式,而其中大部分語言的實現都是基於「類」。css

在JavaScript中並無類這種概念,JS中的函數屬於一等對象,在JS中定義一個對象很是簡單(var obj = {}),而基於JS中閉包與弱類型等特性,在實現一些設計模式的方式上不同凡響。html

本文基於《JavaScript設計模式與開發實踐》一書,用一些例子總結一下JS常見的設計模式與實現方法。文章略長,自備瓜子板凳~前端

 

設計原則

單一職責原則(SRP)web

一個對象或方法只作一件事情。若是一個方法承擔了過多的職責,那麼在需求的變遷過程當中,須要改寫這個方法的可能性就越大。面試

應該把對象或方法劃分紅較小的粒度算法

最少知識原則(LKP)編程

一個軟件實體應當 儘量少地與其餘實體發生相互做用 後端

應當儘可能減小對象之間的交互。若是兩個對象之間沒必要彼此直接通訊,那麼這兩個對象就不要發生直接的 相互聯繫,能夠轉交給第三方進行處理設計模式

開放-封閉原則(OCP)數組

軟件實體(類、模塊、函數)等應該是能夠 擴展的,可是不可修改

當須要改變一個程序的功能或者給這個程序增長新功能的時候,可使用增長代碼的方式,儘可能避免改動程序的源代碼,防止影響原系統的穩定

 

什麼是設計模式

做者的這個說明解釋得挺好

假設有一個空房間,咱們要日復一日地往裏 面放一些東西。最簡單的辦法固然是把這些東西 直接扔進去,可是時間久了,就會發現很難從這 個房子裏找到本身想要的東西,要調整某幾樣東 西的位置也不容易。因此在房間裏作一些櫃子也 許是個更好的選擇,雖然櫃子會增長咱們的成 本,但它能夠在維護階段爲咱們帶來好處。使用 這些櫃子存放東西的規則,或許就是一種模式

學習設計模式,有助於寫出可複用和可維護性高的程序

設計模式的原則是「找出 程序中變化的地方,並將變化封裝起來」,它的關鍵是意圖,而不是結構。

不過要注意,使用不當的話,可能會事倍功半。

 

1、單例模式

2、策略模式

3、代理模式

4、迭代器模式

5、發佈—訂閱模式

6、命令模式

7、組合模式

8、模板方法模式

9、享元模式

10、職責鏈模式

11、中介者模式

12、裝飾者模式

十3、狀態模式

十4、適配器模式

十5、外觀模式

 

1、單例模式

1. 定義

保證一個類僅有一個實例,並提供一個訪問它的全局訪問點

2. 核心

確保只有一個實例,並提供全局訪問

3. 實現

假設要設置一個管理員,屢次調用也僅設置一次,咱們可使用閉包緩存一個內部變量來實現這個單例

function SetManager(name) {
    this.manager = name;
}

SetManager.prototype.getName = function() {
    console.log(this.manager);
};

var SingletonSetManager = (function() {
    var manager = null;

    return function(name) {
        if (!manager) {
            manager = new SetManager(name);
        }

        return manager;
    } 
})();

SingletonSetManager('a').getName(); // a
SingletonSetManager('b').getName(); // a
SingletonSetManager('c').getName(); // a

這是比較簡單的作法,可是假如咱們還要設置一個HR呢?就得複製一遍代碼了

因此,能夠改寫單例內部,實現地更通用一些

// 提取出通用的單例
function getSingleton(fn) {
    var instance = null;

    return function() {
        if (!instance) {
            instance = fn.apply(this, arguments);
        }

        return instance;
    }
}

再進行調用,結果仍是同樣

// 獲取單例
var managerSingleton = getSingleton(function(name) {
    var manager = new SetManager(name);
    return manager;
});

managerSingleton('a').getName(); // a
managerSingleton('b').getName(); // a
managerSingleton('c').getName(); // a

這時,咱們添加HR時,就不須要更改獲取單例內部的實現了,僅須要實現添加HR所須要作的,再調用便可

function SetHr(name) {
    this.hr = name;
}

SetHr.prototype.getName = function() {
    console.log(this.hr);
};

var hrSingleton = getSingleton(function(name) {
    var hr = new SetHr(name);
    return hr;
});

hrSingleton('aa').getName(); // aa
hrSingleton('bb').getName(); // aa
hrSingleton('cc').getName(); // aa

或者,僅想要建立一個div層,不須要將對象實例化,直接調用函數

結果爲頁面中僅有第一個建立的div

function createPopup(html) {
    var div = document.createElement('div');
    div.innerHTML = html;
    document.body.append(div);

    return div;
}

var popupSingleton = getSingleton(function() {
    var div = createPopup.apply(this, arguments);
    return div;
});

console.log(
    popupSingleton('aaa').innerHTML,
    popupSingleton('bbb').innerHTML,
    popupSingleton('bbb').innerHTML
); // aaa  aaa  aaa

 

2、策略模式

1. 定義

定義一系列的算法,把它們一個個封裝起來,而且使它們能夠相互替換。

2. 核心

將算法的使用和算法的實現分離開來。

一個基於策略模式的程序至少由兩部分組成:

第一個部分是一組策略類,策略類封裝了具體的算法,並負責具體的計算過程。

第二個部分是環境類Context,Context接受客戶的請求,隨後把請求委託給某一個策略類。要作到這點,說明Context 中要維持對某個策略對象的引用

3. 實現

策略模式能夠用於組合一系列算法,也可用於組合一系列業務規則

假設須要經過成績等級來計算學生的最終得分,每一個成績等級有對應的加權值。咱們能夠利用對象字面量的形式直接定義這個組策略

// 加權映射關係
var levelMap = {
    S: 10,
    A: 8,
    B: 6,
    C: 4
};

// 組策略
var scoreLevel = {
    basicScore: 80,

    S: function() {
        return this.basicScore + levelMap['S']; 
    },

    A: function() {
        return this.basicScore + levelMap['A']; 
    },

    B: function() {
        return this.basicScore + levelMap['B']; 
    },

    C: function() {
        return this.basicScore + levelMap['C']; 
    }
}

// 調用
function getScore(level) {
    return scoreLevel[level] ? scoreLevel[level]() : 0;
}

console.log(
    getScore('S'),
    getScore('A'),
    getScore('B'),
    getScore('C'),
    getScore('D')
); // 90 88 86 84 0

在組合業務規則方面,比較經典的是表單的驗證方法。這裏列出比較關鍵的部分

// 錯誤提示
var errorMsgs = {
    default: '輸入數據格式不正確',
    minLength: '輸入數據長度不足',
    isNumber: '請輸入數字',
    required: '內容不爲空'
};

// 規則集
var rules = {
    minLength: function(value, length, errorMsg) {
        if (value.length < length) {
            return errorMsg || errorMsgs['minLength']
        }
    },
    isNumber: function(value, errorMsg) {
        if (!/\d+/.test(value)) {
            return errorMsg || errorMsgs['isNumber'];
        }
    },
    required: function(value, errorMsg) {
        if (value === '') {
            return errorMsg || errorMsgs['required'];
        }
    }
};

// 校驗器
function Validator() {
    this.items = [];
};

Validator.prototype = {
    constructor: Validator,
    
    // 添加校驗規則
    add: function(value, rule, errorMsg) {
        var arg = [value];

        if (rule.indexOf('minLength') !== -1) {
            var temp = rule.split(':');
            arg.push(temp[1]);
            rule = temp[0];
        }

        arg.push(errorMsg);

        this.items.push(function() {
            // 進行校驗
            return rules[rule].apply(this, arg);
        });
    },
    
    // 開始校驗
    start: function() {
        for (var i = 0; i < this.items.length; ++i) {
            var ret = this.items[i]();
            
            if (ret) {
                console.log(ret);
                // return ret;
            }
        }
    }
};

// 測試數據
function testTel(val) {
    return val;
}

var validate = new Validator();

validate.add(testTel('ccc'), 'isNumber', '只能爲數字'); // 只能爲數字
validate.add(testTel(''), 'required'); // 內容不爲空
validate.add(testTel('123'), 'minLength:5', '最少5位'); // 最少5位
validate.add(testTel('12345'), 'minLength:5', '最少5位');

var ret = validate.start();

console.log(ret);

4. 優缺點

優勢

能夠有效地避免多重條件語句,將一系列方法封裝起來也更直觀,利於維護

缺點

每每策略集會比較多,咱們須要事先就瞭解定義好全部的狀況

 

3、代理模式

1. 定義

爲一個對象提供一個代用品或佔位符,以便控制對它的訪問

2. 核心

當客戶不方便直接訪問一個 對象或者不知足須要的時候,提供一個替身對象 來控制對這個對象的訪問,客戶實際上訪問的是 替身對象。

替身對象對請求作出一些處理以後, 再把請求轉交給本體對象

代理和本體的接口具備一致性,本體定義了關鍵功能,而代理是提供或拒絕對它的訪問,或者在訪問本體以前作一 些額外的事情

3. 實現

代理模式主要有三種:保護代理、虛擬代理、緩存代理

保護代理主要實現了訪問主體的限制行爲,以過濾字符做爲簡單的例子

// 主體,發送消息
function sendMsg(msg) {
    console.log(msg);
}

// 代理,對消息進行過濾
function proxySendMsg(msg) {
    // 無消息則直接返回
    if (typeof msg === 'undefined') {
        console.log('deny');
        return;
    }
    
    // 有消息則進行過濾
    msg = ('' + msg).replace(/泥\s*煤/g, '');

    sendMsg(msg);
}


sendMsg('泥煤呀泥 煤呀'); // 泥煤呀泥 煤呀
proxySendMsg('泥煤呀泥 煤'); //
proxySendMsg(); // deny

它的意圖很明顯,在訪問主體以前進行控制,沒有消息的時候直接在代理中返回了,拒絕訪問主體,這數據保護代理的形式

有消息的時候對敏感字符進行了處理,這屬於虛擬代理的模式

 

虛擬代理在控制對主體的訪問時,加入了一些額外的操做

在滾動事件觸發的時候,也許不須要頻繁觸發,咱們能夠引入函數節流,這是一種虛擬代理的實現

// 函數防抖,頻繁操做中不處理,直到操做完成以後(再過 delay 的時間)才一次性處理
function debounce(fn, delay) {
    delay = delay || 200;
    
    var timer = null;

    return function() {
        var arg = arguments;
          
        // 每次操做時,清除上次的定時器
        clearTimeout(timer);
        timer = null;
        
        // 定義新的定時器,一段時間後進行操做
        timer = setTimeout(function() {
            fn.apply(this, arg);
        }, delay);
    }
};

var count = 0;

// 主體
function scrollHandle(e) {
    console.log(e.type, ++count); // scroll
}

// 代理
var proxyScrollHandle = (function() {
    return debounce(scrollHandle, 500);
})();

window.onscroll = proxyScrollHandle;

 

緩存代理能夠爲一些開銷大的運算結果提供暫時的緩存,提高效率

來個栗子,緩存加法操做

// 主體
function add() {
    var arg = [].slice.call(arguments);

    return arg.reduce(function(a, b) {
        return a + b;
    });
}

// 代理
var proxyAdd = (function() {
    var cache = [];

    return function() {
        var arg = [].slice.call(arguments).join(',');
        
        // 若是有,則直接從緩存返回
        if (cache[arg]) {
            return cache[arg];
        } else {
            var ret = add.apply(this, arguments);
            return ret;
        }
    };
})();

console.log(
    add(1, 2, 3, 4),
    add(1, 2, 3, 4),

    proxyAdd(10, 20, 30, 40),
    proxyAdd(10, 20, 30, 40)
); // 10 10 100 100

 

4、迭代器模式

1. 定義

迭代器模式是指提供一種方法順序訪問一個聚合對象中的各個元素,而又不須要暴露該對象的內部表示。

2. 核心

在使用迭代器模式以後,即便不關心對象的內部構造,也能夠按順序訪問其中的每一個元素

3. 實現

JS中數組的map forEach 已經內置了迭代器

[1, 2, 3].forEach(function(item, index, arr) {
    console.log(item, index, arr);
});

不過對於對象的遍歷,每每不能與數組同樣使用同一的遍歷代碼

咱們能夠封裝一下

function each(obj, cb) {
    var value;

    if (Array.isArray(obj)) {
        for (var i = 0; i < obj.length; ++i) {
            value = cb.call(obj[i], i, obj[i]);

            if (value === false) {
                break;
            }
        }
    } else {
        for (var i in obj) {
            value = cb.call(obj[i], i, obj[i]);

            if (value === false) {
                break;
            }
        }
    }
}

each([1, 2, 3], function(index, value) {
    console.log(index, value);
});

each({a: 1, b: 2}, function(index, value) {
    console.log(index, value);
});

// 0 1
// 1 2
// 2 3

// a 1
// b 2

再來看一個例子,強行地使用迭代器,來了解一下迭代器也能夠替換頻繁的條件語句

雖然例子不太好,但在其餘負責的分支判斷狀況下,也是值得考慮的

function getManager() {
    var year = new Date().getFullYear();

    if (year <= 2000) {
        console.log('A');
    } else if (year >= 2100) {
        console.log('C');
    } else {
        console.log('B');
    }
}

getManager(); // B

將每一個條件語句拆分出邏輯函數,放入迭代器中迭代

function year2000() {
    var year = new Date().getFullYear();

    if (year <= 2000) {
        console.log('A');
    }

    return false;
}

function year2100() {
    var year = new Date().getFullYear();

    if (year >= 2100) {
        console.log('C');
    }

    return false;
}

function year() {
    var year = new Date().getFullYear();

    if (year > 2000 && year < 2100) {
        console.log('B');
    }

    return false;
}

function iteratorYear() {
    for (var i = 0; i < arguments.length; ++i) {
        var ret = arguments[i]();

        if (ret !== false) {
            return ret;
        }
    }
}

var manager = iteratorYear(year2000, year2100, year); // B

 

5、發佈-訂閱模式

1. 定義

也稱做觀察者模式,定義了對象間的一種一對多的依賴關係,當一個對象的狀態發 生改變時,全部依賴於它的對象都將獲得通知

2. 核心

取代對象之間硬編碼的通知機制,一個對象不用再顯式地調用另一個對象的某個接口。

與傳統的發佈-訂閱模式實現方式(將訂閱者自身當成引用傳入發佈者)不一樣,在JS中一般使用註冊回調函數的形式來訂閱

3. 實現

JS中的事件就是經典的發佈-訂閱模式的實現

// 訂閱
document.body.addEventListener('click', function() {
    console.log('click1');
}, false);

document.body.addEventListener('click', function() {
    console.log('click2');
}, false);

// 發佈
document.body.click(); // click1  click2

本身實現一下

小A在公司C完成了筆試及面試,小B也在公司C完成了筆試。他們焦急地等待結果,每隔半天就電話詢問公司C,致使公司C很不耐煩。

一種解決辦法是 AB直接把聯繫方式留給C,有結果的話C天然會通知AB

這裏的「詢問」屬於顯示調用,「留給」屬於訂閱,「通知」屬於發佈

// 觀察者
var observer = {
    // 訂閱集合
    subscribes: [],

    // 訂閱
    subscribe: function(type, fn) {
        if (!this.subscribes[type]) {
            this.subscribes[type] = [];
        }
        
        // 收集訂閱者的處理
        typeof fn === 'function' && this.subscribes[type].push(fn);
    },

    // 發佈  可能會攜帶一些信息發佈出去
    publish: function() {
        var type = [].shift.call(arguments),
            fns = this.subscribes[type];
        
        // 不存在的訂閱類型,以及訂閱時未傳入處理回調的
        if (!fns || !fns.length) {
            return;
        }
        
        // 挨個處理調用
        for (var i = 0; i < fns.length; ++i) {
            fns[i].apply(this, arguments);
        }
    },
    
    // 刪除訂閱
    remove: function(type, fn) {
        // 刪除所有
        if (typeof type === 'undefined') {
            this.subscribes = [];
            return;
        }

        var fns = this.subscribes[type];

        // 不存在的訂閱類型,以及訂閱時未傳入處理回調的
        if (!fns || !fns.length) {
            return;
        }

        if (typeof fn === 'undefined') {
            fns.length = 0;
            return;
        }

        // 挨個處理刪除
        for (var i = 0; i < fns.length; ++i) {
            if (fns[i] === fn) {
                fns.splice(i, 1);
            }
        }
    }
};

// 訂閱崗位列表
function jobListForA(jobs) {
    console.log('A', jobs);
}

function jobListForB(jobs) {
    console.log('B', jobs);
}

// A訂閱了筆試成績
observer.subscribe('job', jobListForA);
// B訂閱了筆試成績
observer.subscribe('job', jobListForB);


// A訂閱了筆試成績
observer.subscribe('examinationA', function(score) {
    console.log(score);
});

// B訂閱了筆試成績
observer.subscribe('examinationB', function(score) {
    console.log(score);
});

// A訂閱了面試結果
observer.subscribe('interviewA', function(result) {
    console.log(result);
});

observer.publish('examinationA', 100); // 100
observer.publish('examinationB', 80); // 80
observer.publish('interviewA', '備用'); // 備用

observer.publish('job', ['前端', '後端', '測試']); // 輸出A和B的崗位


// B取消訂閱了筆試成績
observer.remove('examinationB');
// A都取消訂閱了崗位
observer.remove('job', jobListForA);

observer.publish('examinationB', 80); // 沒有可匹配的訂閱,無輸出
observer.publish('job', ['前端', '後端', '測試']); // 輸出B的崗位

4. 優缺點

優勢

一爲時間上的解耦,二爲對象之間的解耦。能夠用在異步編程中與MV*框架中

缺點

建立訂閱者自己要消耗必定的時間和內存,訂閱的處理函數不必定會被執行,駐留內存有性能開銷

弱化了對象之間的聯繫,複雜的狀況下可能會致使程序難以跟蹤維護和理解

 

6、命令模式

1. 定義

用一種鬆耦合的方式來設計程序,使得請求發送者和請求接收者可以消除彼此之間的耦合關係

命令(command)指的是一個執行某些特定事情的指令

2. 核心

命令中帶有execute執行、undo撤銷、redo重作等相關命令方法,建議顯示地指示這些方法名

3. 實現

簡單的命令模式實現能夠直接使用對象字面量的形式定義一個命令

var incrementCommand = {
    execute: function() {
        // something
    }
};

不過接下來的例子是一個自增命令,提供執行、撤銷、重作功能

採用對象建立處理的方式,定義這個自增

// 自增
function IncrementCommand() {
    // 當前值
    this.val = 0;
    // 命令棧
    this.stack = [];
    // 棧指針位置
    this.stackPosition = -1;
};

IncrementCommand.prototype = {
    constructor: IncrementCommand,

    // 執行
    execute: function() {
        this._clearRedo();
        
        // 定義執行的處理
        var command = function() {
            this.val += 2;
        }.bind(this);
        
        // 執行並緩存起來
        command();
        
        this.stack.push(command);

        this.stackPosition++;

        this.getValue();
    },
    
    canUndo: function() {
        return this.stackPosition >= 0;
    },
    
    canRedo: function() {
        return this.stackPosition < this.stack.length - 1;
    },

    // 撤銷
    undo: function() {
        if (!this.canUndo()) {
            return;
        }
        
        this.stackPosition--;

        // 命令的撤銷,與執行的處理相反
        var command = function() {
            this.val -= 2;
        }.bind(this);
        
        // 撤銷後不須要緩存
        command();

        this.getValue();
    },
    
    // 重作
    redo: function() {
        if (!this.canRedo()) {
            return;
        }
        
        // 執行棧頂的命令
        this.stack[++this.stackPosition]();

        this.getValue();
    },
    
    // 在執行時,已經撤銷的部分不能再重作
    _clearRedo: function() {
        this.stack = this.stack.slice(0, this.stackPosition + 1);
    },
    
    // 獲取當前值
    getValue: function() {
        console.log(this.val);
    }
};

再實例化進行測試,模擬執行、撤銷、重作的操做

var incrementCommand = new IncrementCommand();

// 模擬事件觸發,執行命令
var eventTrigger = {
    // 某個事件的處理中,直接調用命令的處理方法
    increment: function() {
        incrementCommand.execute();
    },

    incrementUndo: function() {
        incrementCommand.undo();
    },

    incrementRedo: function() {
        incrementCommand.redo();
    }
};


eventTrigger['increment'](); // 2
eventTrigger['increment'](); // 4

eventTrigger['incrementUndo'](); // 2

eventTrigger['increment'](); // 4

eventTrigger['incrementUndo'](); // 2
eventTrigger['incrementUndo'](); // 0
eventTrigger['incrementUndo'](); // 無輸出

eventTrigger['incrementRedo'](); // 2
eventTrigger['incrementRedo'](); // 4
eventTrigger['incrementRedo'](); // 無輸出

eventTrigger['increment'](); // 6

 

此外,還能夠實現簡單的宏命令(一系列命令的集合)

var MacroCommand = {
    commands: [],

    add: function(command) {
        this.commands.push(command);

        return this;
    },

    remove: function(command) {
        if (!command) {
            this.commands = [];
            return;
        }

        for (var i = 0; i < this.commands.length; ++i) {
            if (this.commands[i] === command) {
                this.commands.splice(i, 1);
            }
        }
    },

    execute: function() {
        for (var i = 0; i < this.commands.length; ++i) {
            this.commands[i].execute();
        }
    }
};

var showTime = {
    execute: function() {
        console.log('time');
    }
};

var showName = {
    execute: function() {
        console.log('name');
    }
};

var showAge = {
    execute: function() {
        console.log('age');
    }
};

MacroCommand.add(showTime).add(showName).add(showAge);

MacroCommand.remove(showName);

MacroCommand.execute(); // time age

 

7、組合模式

1. 定義

是用小的子對象來構建更大的 對象,而這些小的子對象自己也許是由更小 的「孫對象」構成的。

2. 核心

能夠用樹形結構來表示這種「部分- 總體」的層次結構。

調用組合對象 的execute方法,程序會遞歸調用組合對象 下面的葉對象的execute方法

但要注意的是,組合模式不是父子關係,它是一種HAS-A(聚合)的關係,將請求委託給 它所包含的全部葉對象。基於這種委託,就須要保證組合對象和葉對象擁有相同的 接口

此外,也要保證用一致的方式對待 列表中的每一個葉對象,即葉對象屬於同一類,不須要過多特殊的額外操做

3. 實現

使用組合模式來實現掃描文件夾中的文件

// 文件夾 組合對象
function Folder(name) {
    this.name = name;
    this.parent = null;
    this.files = [];
}

Folder.prototype = {
    constructor: Folder,

    add: function(file) {
        file.parent = this;
        this.files.push(file);

        return this;
    },

    scan: function() {
        // 委託給葉對象處理
        for (var i = 0; i < this.files.length; ++i) {
            this.files[i].scan();
        }
    },

    remove: function(file) {
        if (typeof file === 'undefined') {
            this.files = [];
            return;
        }

        for (var i = 0; i < this.files.length; ++i) {
            if (this.files[i] === file) {
                this.files.splice(i, 1);
            }
        }
    }
};

// 文件 葉對象
function File(name) {
    this.name = name;
    this.parent = null;
}

File.prototype = {
    constructor: File,

    add: function() {
        console.log('文件裏面不能添加文件');
    },

    scan: function() {
        var name = [this.name];
        var parent = this.parent;

        while (parent) {
            name.unshift(parent.name);
            parent = parent.parent;
        }

        console.log(name.join(' / '));
    }
};

構造好組合對象與葉對象的關係後,實例化,在組合對象中插入組合或葉對象

var web = new Folder('Web');
var fe = new Folder('前端');
var css = new Folder('CSS');
var js = new Folder('js');
var rd = new Folder('後端');

web.add(fe).add(rd);

var file1 = new File('HTML權威指南.pdf');
var file2 = new File('CSS權威指南.pdf');
var file3 = new File('JavaScript權威指南.pdf');
var file4 = new File('MySQL基礎.pdf');
var file5 = new File('Web安全.pdf');
var file6 = new File('Linux菜鳥.pdf');

css.add(file2);
fe.add(file1).add(file3).add(css).add(js);
rd.add(file4).add(file5);
web.add(file6);

rd.remove(file4);

// 掃描
web.scan();

掃描結果爲

 

 4. 優缺點

優勢

可 以方便地構造一棵樹來表示對象的部分-總體 結構。在樹的構造最終 完成以後,只須要經過請求樹的最頂層對 象,便能對整棵樹作統一一致的操做。

缺點

建立出來的對象長得都差很少,可能會使代碼很差理解,建立太多的對象對性能也會有一些影響

 

8、模板方法模式

1. 定義

模板方法模式由兩部分結構組成,第一部分是抽象父類,第二部分是具體的實現子類。

2. 核心

在抽象父類中封裝子類的算法框架,它的 init方法可做爲一個算法的模板,指導子類以何種順序去執行哪些方法。

由父類分離出公共部分,要求子類重寫某些父類的(易變化的)抽象方法

3. 實現

模板方法模式通常的實現方式爲繼承

以運動做爲例子,運動有比較通用的一些處理,這部分能夠抽離開來,在父類中實現。具體某項運動的特殊性則有自類來重寫實現。

最終子類直接調用父類的模板函數來執行

// 體育運動
function Sport() {

}

Sport.prototype = {
    constructor: Sport,
    
    // 模板,按順序執行
    init: function() {
        this.stretch();
        this.jog();
        this.deepBreath();
        this.start();

        var free = this.end();
        
        // 運動後還有空的話,就拉伸一下
        if (free !== false) {
            this.stretch();
        }
        
    },
    
    // 拉伸
    stretch: function() {
        console.log('拉伸');
    },
    
    // 慢跑
    jog: function() {
        console.log('慢跑');
    },
    
    // 深呼吸
    deepBreath: function() {
        console.log('深呼吸');
    },

    // 開始運動
    start: function() {
        throw new Error('子類必須重寫此方法');
    },

    // 結束運動
    end: function() {
        console.log('運動結束');
    }
};

// 籃球
function Basketball() {

}

Basketball.prototype = new Sport();

// 重寫相關的方法
Basketball.prototype.start = function() {
    console.log('先投上幾個三分');
};

Basketball.prototype.end = function() {
    console.log('運動結束了,有事先走一步');
    return false;
};


// 馬拉松
function Marathon() {

}

Marathon.prototype = new Sport();

var basketball = new Basketball();
var marathon = new Marathon();

// 子類調用,最終會按照父類定義的順序執行
basketball.init();
marathon.init();

 

9、享元模式

1. 定義

享元(flyweight)模式是一種用於性能優化的模式,它的目標是儘可能減小共享對象的數量

2. 核心

運用共享技術來有效支持大量細粒度的對象。

強調將對象的屬性劃分爲內部狀態(屬性)與外部狀態(屬性)。內部狀態用於對象的共享,一般不變;而外部狀態則剝離開來,由具體的場景決定。

3. 實現

在程序中使用了大量的類似對象時,能夠利用享元模式來優化,減小對象的數量

舉個栗子,要對某個班進行身體素質測量,僅測量身高體重來評判

// 健康測量
function Fitness(name, sex, age, height, weight) {
    this.name = name;
    this.sex = sex;
    this.age = age;
    this.height = height;
    this.weight = weight;
}

// 開始評判
Fitness.prototype.judge = function() {
    var ret = this.name + ': ';

    if (this.sex === 'male') {
        ret += this.judgeMale();
    } else {
        ret += this.judgeFemale();
    }

    console.log(ret);
};

// 男性評判規則
Fitness.prototype.judgeMale = function() {
    var ratio = this.height / this.weight;

    return this.age > 20 ? (ratio > 3.5) : (ratio > 2.8);
};

// 女性評判規則
Fitness.prototype.judgeFemale = function() {
    var ratio = this.height / this.weight;
    
    return this.age > 20 ? (ratio > 4) : (ratio > 3);
};


var a = new Fitness('A', 'male', 18, 160, 80);
var b = new Fitness('B', 'male', 21, 180, 70);
var c = new Fitness('C', 'female', 28, 160, 80);
var d = new Fitness('D', 'male', 18, 170, 60);
var e = new Fitness('E', 'female', 18, 160, 40);

// 開始評判
a.judge(); // A: false
b.judge(); // B: false
c.judge(); // C: false
d.judge(); // D: true
e.judge(); // E: true

評判五我的就須要建立五個對象,一個班就幾十個對象

能夠將對象的公共部分(內部狀態)抽離出來,與外部狀態獨立。將性別看作內部狀態便可,其餘屬性都屬於外部狀態。

這麼一來咱們只須要維護男和女兩個對象(使用factory對象),而其餘變化的部分則在外部維護(使用manager對象)

// 健康測量
function Fitness(sex) {
    this.sex = sex;
}

// 工廠,建立可共享的對象
var FitnessFactory = {
    objs: [],

    create: function(sex) {
        if (!this.objs[sex]) {
            this.objs[sex] = new Fitness(sex);
        }

        return this.objs[sex];
    }
};

// 管理器,管理非共享的部分
var FitnessManager = {
    fitnessData: {},
    
    // 添加一項
    add: function(name, sex, age, height, weight) {
        var fitness = FitnessFactory.create(sex);
        
        // 存儲變化的數據
        this.fitnessData[name] = {
            age: age,
            height: height,
            weight: weight
        };

        return fitness;
    },
    
    // 從存儲的數據中獲取,更新至當前正在使用的對象
    updateFitnessData: function(name, obj) {
        var fitnessData = this.fitnessData[name];

        for (var item in fitnessData) {
            if (fitnessData.hasOwnProperty(item)) {
                obj[item] = fitnessData[item];
            }
        }
    }
};

// 開始評判
Fitness.prototype.judge = function(name) {
    // 操做前先更新當前狀態(從外部狀態管理器中獲取)
    FitnessManager.updateFitnessData(name, this);

    var ret = name + ': ';

    if (this.sex === 'male') {
        ret += this.judgeMale();
    } else {
        ret += this.judgeFemale();
    }

    console.log(ret);
};

// 男性評判規則
Fitness.prototype.judgeMale = function() {
    var ratio = this.height / this.weight;

    return this.age > 20 ? (ratio > 3.5) : (ratio > 2.8);
};

// 女性評判規則
Fitness.prototype.judgeFemale = function() {
    var ratio = this.height / this.weight;
    
    return this.age > 20 ? (ratio > 4) : (ratio > 3);
};


var a = FitnessManager.add('A', 'male', 18, 160, 80);
var b = FitnessManager.add('B', 'male', 21, 180, 70);
var c = FitnessManager.add('C', 'female', 28, 160, 80);
var d = FitnessManager.add('D', 'male', 18, 170, 60);
var e = FitnessManager.add('E', 'female', 18, 160, 40);

// 開始評判
a.judge('A'); // A: false
b.judge('B'); // B: false
c.judge('C'); // C: false
d.judge('D'); // D: true
e.judge('E'); // E: true

不過代碼可能更復雜了,這個例子可能還不夠充分,只是展現了享元模式如何實現,它節省了多個類似的對象,但多了一些操做。

factory對象有點像單例模式,只是多了一個sex的參數,若是沒有內部狀態,則沒有參數的factory對象就更接近單例模式了

 

10、職責鏈模式

1. 定義

使多個對象都有機會處理請求,從而避免請求的發送者和接收者之間的耦合關係,將這些對象連成一條鏈,並沿着這條鏈 傳遞該請求,直到有一個對象處理它爲止

2. 核心

請求發送者只須要知道鏈中的第一個節點,弱化發送者和一組接收者之間的強聯繫,能夠便捷地在職責鏈中增長或刪除一個節點,一樣地,指定誰是第一個節點也很便捷

3. 實現

以展現不一樣類型的變量爲例,設置一條職責鏈,能夠免去多重if條件分支

// 定義鏈的某一項
function ChainItem(fn) {
    this.fn = fn;
    this.next = null;
}

ChainItem.prototype = {
    constructor: ChainItem,
    
    // 設置下一項
    setNext: function(next) {
        this.next = next;
        return next;
    },
    
    // 開始執行
    start: function() {
        this.fn.apply(this, arguments);
    },
    
    // 轉到鏈的下一項執行
    toNext: function() {
        if (this.next) {
            this.start.apply(this.next, arguments);
        } else {
            console.log('無匹配的執行項目');
        }
    }
};

// 展現數字
function showNumber(num) {
    if (typeof num === 'number') {
        console.log('number', num);
    } else {
        // 轉移到下一項
        this.toNext(num);
    }
}

// 展現字符串
function showString(str) {
    if (typeof str === 'string') {
        console.log('string', str);
    } else {
        this.toNext(str);
    }
}

// 展現對象
function showObject(obj) {
    if (typeof obj === 'object') {
        console.log('object', obj);
    } else {
        this.toNext(obj);
    }
}

var chainNumber = new ChainItem(showNumber);
var chainString = new ChainItem(showString);
var chainObject = new ChainItem(showObject);

// 設置鏈條
chainObject.setNext(chainNumber).setNext(chainString);

chainString.start('12'); // string 12
chainNumber.start({}); // 無匹配的執行項目
chainObject.start({}); // object {}
chainObject.start(123); // number 123

這時想判斷未定義的時候呢,直接加到鏈中便可

// 展現未定義
function showUndefined(obj) {
    if (typeof obj === 'undefined') {
        console.log('undefined');
    } else {
        this.toNext(obj);
    }
}

var chainUndefined = new ChainItem(showUndefined);
chainString.setNext(chainUndefined);

chainNumber.start(); // undefined

由例子能夠看到,使用了職責鏈後,由本來的條件分支換成了不少對象,雖然結構更加清晰了,但在必定程度上可能會影響到性能,因此要注意避免過長的職責鏈。

 

11、中介者模式

1. 定義

全部的相關 對象都經過中介者對象來通訊,而不是互相引用,因此當一個對象發生改變時,只須要通知中介者對象便可

2. 核心

使網狀的多對多關係變成了相對簡單的一對多關係(複雜的調度處理都交給中介者)

使用中介者後

 

 

 

 

3. 實現

多個對象,指的不必定得是實例化的對象,也能夠將其理解成互爲獨立的多個項。當這些項在處理時,須要知曉並經過其餘項的數據來處理。

若是每一個項都直接處理,程序會很是複雜,修改某個地方就得在多個項內部修改

咱們將這個處理過程抽離出來,封裝成中介者來處理,各項須要處理時,通知中介者便可。

var A = {
    score: 10,

    changeTo: function(score) {
        this.score = score;

        // 本身獲取
        this.getRank();
    },
    
    // 直接獲取
    getRank: function() {
        var scores = [this.score, B.score, C.score].sort(function(a, b) {
            return a < b;
        });

        console.log(scores.indexOf(this.score) + 1);
    }
};

var B = {
    score: 20,

    changeTo: function(score) {
        this.score = score;

        // 經過中介者獲取
        rankMediator(B);
    }
};

var C = {
    score: 30,

    changeTo: function(score) {
        this.score = score;

        rankMediator(C);
    }
};

// 中介者,計算排名
function rankMediator(person) {
    var scores = [A.score, B.score, C.score].sort(function(a, b) {
        return a < b;
    });

    console.log(scores.indexOf(person.score) + 1);
}

// A經過自身來處理
A.changeTo(100); // 1

// B和C交由中介者處理
B.changeTo(200); // 1
C.changeTo(50); // 3

ABC三我的分數改變後想要知道本身的排名,在A中本身處理,而B和C使用了中介者。B和C將更爲輕鬆,總體代碼也更簡潔

最後,雖然中介者作到了對模塊和對象的解耦,但有時對象之間的關係並不是必定要解耦,強行使用中介者來整合,可能會使代碼更爲繁瑣,須要注意。

 

12、裝飾者模式

1. 定義

以動態地給某個對象添加一些額外的職責,而不會影響從這個類中派生的其餘對象。
是一種「即用即付」的方式,可以在不改變對 象自身的基礎上,在程序運行期間給對象動態地 添加職責

2. 核心

是爲對象動態加入行爲,通過多重包裝,能夠造成一條裝飾鏈

3. 實現

最簡單的裝飾者,就是重寫對象的屬性

var A = {
    score: 10
};

A.score = '分數:' + A.score;

可使用傳統面向對象的方法來實現裝飾,添加技能

function Person() {}

Person.prototype.skill = function() {
    console.log('數學');
};

// 裝飾器,還會音樂
function MusicDecorator(person) {
    this.person = person;
}

MusicDecorator.prototype.skill = function() {
    this.person.skill();
    console.log('音樂');
};

// 裝飾器,還會跑步
function RunDecorator(person) {
    this.person = person;
}

RunDecorator.prototype.skill = function() {
    this.person.skill();
    console.log('跑步');
};

var person = new Person();

// 裝飾一下
var person1 = new MusicDecorator(person);
person1 = new RunDecorator(person1);

person.skill(); // 數學
person1.skill(); // 數學 音樂 跑步

在JS中,函數爲一等對象,因此咱們也可使用更通用的裝飾函數

// 裝飾器,在當前函數執行前先執行另外一個函數
function decoratorBefore(fn, beforeFn) {
    return function() {
        var ret = beforeFn.apply(this, arguments);
        
        // 在前一個函數中判斷,不須要執行當前函數
        if (ret !== false) {
            fn.apply(this, arguments);
        }
    };
}


function skill() {
    console.log('數學');
}

function skillMusic() {
    console.log('音樂');
}

function skillRun() {
    console.log('跑步');
}

var skillDecorator = decoratorBefore(skill, skillMusic);
skillDecorator = decoratorBefore(skillDecorator, skillRun);

skillDecorator(); // 跑步 音樂 數學

 

十3、狀態模式

1. 定義

事物內部狀態的改變每每會帶來事物的行爲改變。在處理的時候,將這個處理委託給當前的狀態對象便可,該狀態對象會負責渲染它自身的行爲

2. 核心

區分事物內部的狀態,把事物的每種狀態都封裝成單獨的類,跟此種狀態有關的行爲都被封裝在這個類的內部

3. 實現

以一我的的工做狀態做爲例子,在剛醒、精神、疲倦幾個狀態中切換着

// 工做狀態
function Work(name) {
    this.name = name;
    this.currentState = null;

    // 工做狀態,保存爲對應狀態對象
    this.wakeUpState = new WakeUpState(this);
    // 精神飽滿
    this.energeticState = new EnergeticState(this);
    // 疲倦
    this.tiredState = new TiredState(this);

    this.init();
}

Work.prototype.init = function() {
    this.currentState = this.wakeUpState;
    
    // 點擊事件,用於觸發更新狀態
    document.body.onclick = () => {
        this.currentState.behaviour();
    };
};

// 更新工做狀態
Work.prototype.setState = function(state) {
    this.currentState = state;
}

// 剛醒
function WakeUpState(work) {
    this.work = work;
}

// 剛醒的行爲
WakeUpState.prototype.behaviour = function() {
    console.log(this.work.name, ':', '剛醒呢,睡個懶覺先');
    
    // 只睡了2秒鐘懶覺就精神了..
    setTimeout(() => {
        this.work.setState(this.work.energeticState);
    }, 2 * 1000);
}

// 精神飽滿
function EnergeticState(work) {
    this.work = work;
}

EnergeticState.prototype.behaviour = function() {
    console.log(this.work.name, ':', '超級精神的');
    
    // 才精神1秒鐘就發睏了
    setTimeout(() => {
        this.work.setState(this.work.tiredState);
    }, 1000);
};

// 疲倦
function TiredState(work) {
    this.work = work;
}

TiredState.prototype.behaviour = function() {
    console.log(this.work.name, ':', '怎麼肥事,好睏');
    
    // 不知不覺,又變成了剛醒着的狀態... 不斷循環呀
    setTimeout(() => {
        this.work.setState(this.work.wakeUpState);
    }, 1000);
};


var work = new Work('曹操');

點擊一下頁面,觸發更新狀態的操做

 

4. 優缺點

優勢

狀態切換的邏輯分佈在狀態類中,易於維護

缺點

多個狀態類,對於性能來講,也是一個缺點,這個缺點可使用享元模式來作進一步優化

將邏輯分散在狀態類中,可能不會很輕易就能看出狀態機的變化邏輯

 

十4、適配器模式

1. 定義

是解決兩個軟件實體間的接口不兼容的問題,對不兼容的部分進行適配

2. 核心

解決兩個已有接口之間不匹配的問題

3. 實現

好比一個簡單的數據格式轉換的適配器

// 渲染數據,格式限制爲數組了
function renderData(data) {
    data.forEach(function(item) {
        console.log(item);
    });
}

// 對非數組的進行轉換適配
function arrayAdapter(data) {
    if (typeof data !== 'object') {
        return [];
    }

    if (Object.prototype.toString.call(data) === '[object Array]') {
        return data;
    }

    var temp = [];

    for (var item in data) {
        if (data.hasOwnProperty(item)) {
            temp.push(data[item]);
        }
    }

    return temp;
}

var data = {
    0: 'A',
    1: 'B',
    2: 'C'
};

renderData(arrayAdapter(data)); // A B C

 

十5、外觀模式

1. 定義

爲子系統中的一組接口提供一個一致的界面,定義一個高層接口,這個接口使子系統更加容易使用

2. 核心

能夠經過請求外觀接口來達到訪問子系統,也能夠選擇越過外觀來直接訪問子系統

3. 實現

外觀模式在JS中,能夠認爲是一組函數的集合

// 三個處理函數
function start() {
    console.log('start');
}

function doing() {
    console.log('doing');
}

function end() {
    console.log('end');
}

// 外觀函數,將一些處理統一塊兒來,方便調用
function execute() {
    start();
    doing();
    end();
}


// 調用init開始執行
function init() {
    // 此處直接調用了高層函數,也能夠選擇越過它直接調用相關的函數
    execute();
}

init(); // start doing end

 

個人博客即將搬運同步至騰訊雲+社區,邀請你們一同入駐:https://cloud.tencent.com/developer/support-plan?invite_code=2kmd69ge4zeow

相關文章
相關標籤/搜索