1. 程式人生 > 實用技巧 >Javascript中常用的13種設計模式

Javascript中常用的13種設計模式

介紹常用的Javascript設計模式。

常用設計模式分類

常用23 種設計模式可以分為三大類:

  • 建立型模式(Creational Patterns)
  • 結構型模式(Structural Patterns)
  • 行為型模式(Behavioral Patterns)

建立型模式(5種)

這些設計模式提供了一種在建立物件的同時隱藏建立邏輯的方式,而不是使用 new 運算子直接例項化物件。這使得程式在判斷針對某個給定例項需要建立哪些物件時更加靈活。

  • 工廠模式(Factory Pattern)
  • 抽象工廠模式(Abstract Factory Pattern)
  • 單例模式(Singleton Pattern)
  • 建造者模式(Builder Pattern)
  • 原型模式(Prototype Pattern)

結構型模式(7種)

這些設計模式關注類和物件的組合。繼承的概念被用來組合介面和定義組合物件獲得新功能的方式。

  • 介面卡模式(Adapter Pattern)
  • 裝飾器模式(Decorator Pattern)
  • 橋接模式(Bridge Pattern)
  • 代理模式(Proxy Pattern)
  • 外觀模式(Facade Pattern)
  • 組合模式(Composite Pattern)
  • 享元模式(Flyweight Pattern)

行為型模式(11種)

這些設計模式特別關注物件之間的通訊。

  • 策略模式(Strategy Pattern)
  • 模版模式(Template Pattern)
  • 觀察者模式(Observer Pattern)
  • 迭代器模式(Iterator Pattern)
  • 中介者模式(Mediator Pattern)
  • 狀態模式(State Pattern)
  • 職責鏈模式(Chain of Responsibility Pattern)
  • 命令模式(Command Pattern)
  • 備忘錄模式(Memento Pattern)
  • 直譯器模式(Interpreter Pattern)
  • 訪問者模式(Visitor Pattern)

23種設計模式分類圖

單例模式(Singleton Pattern)

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

單例模式是一種常用的模式,有一些物件我們往往只需要一個,比如執行緒池、全域性快取、瀏 覽器中的 window 物件等。在 JavaScript 開發中,單例模式的用途同樣非常廣泛。試想一下,當我們單擊登入按鈕的時候,頁面中會出現一個登入浮窗,而這個登入浮窗是唯一的,無論單擊多少次登入按鈕,這個浮窗都只會被建立一次,那麼這個登入浮窗就適合用單例模式來建立。

實現

簡單實現

/**
 * 單例模式
 */
class Singleton {
    constructor(name) {
        this.name = name;
        this.instance = null;
    }
    getName(){
        console.log(this.name);
    }
    static getInstance(name){
        if (!this.instance){
            this.instance = new Singleton(name);
        }
        return this.instance;
    };
}
const a = Singleton.getInstance('sven1');
const b = Singleton.getInstance('sven2');
console.log(a === b); // true

透明的單例模式

但是這個類需要例項化後呼叫方法才能建立例項,我們希望直接例項化時就能建立。

/**
 * 使用閉包實現單例模式
 */
const Singleton = (function(){
    let instance;
    class SingletonOrigin {
        constructor(name) {
            if (instance) {
                return instance;
            }
            this.name = name;
            instance = this;
            return this;
        }
        getName(){
            console.log(this.name);
        }
    }
    return SingletonOrigin;
})();

const a = new Singleton('sven1');
const b = new Singleton('sven2');
console.log(a === b); // true
複製程式碼

為了把 instance 封裝起來,我們使用了自執行的匿名函式和閉包,並且讓這個匿名函式返回 真正的 Singleton 構造方法,這增加了一些程式的複雜度,閱讀起來也不是很舒服。

使用代理實現單例模式

/**
 * 使用代理實現單例模式
 */
const Singleton = (function (){
    class SingletonOrigin {
        constructor(name) {
            this.name = name;
        }
        getName(){
            console.log(this.name);
        }
    }
    let instance;
    return function(name) {
        if (!instance){
            instance = new SingletonOrigin(name);
        }
        return instance;
    }
})();

const a = new Singleton('sven1');
const b = new Singleton('sven2');
console.log(a === b); // true

惰性單例模式

前面幾種實現方式是基於面向物件思路的實現,現在使用js特殊的方式實現單例模式,名為惰性單例模式

惰性單例模式可以推遲建立物件的時機,並不在一開始就建立,所以叫惰性

/**
 * 惰性單例模式
 */
const getInstance = (function () {
    function createInstance(name) {
        return {
            name: name,
            getName() {
                console.log(this.name);
            }
        }
    }
    function getSingle (fn){
        let result;
        return function() {
            if (!result) {
                result = fn.apply(this, arguments)
            }
            return result;
        }
    };
    return getSingle(createInstance);
})();

const a = getInstance('sven1');
const b = getInstance('sven2');
console.log(a === b); // true

釋出訂閱模式(又叫觀察者模式Observer Pattern)

實現釋出訂閱模式

介紹

釋出—訂閱模式又叫觀察者模式,它定義物件間的一種一對多的依賴關係,當一個物件的狀態發生改變時,所有依賴於它的物件都將得到通知。在JavaScript開發中,一般用事件模型來替代傳統的釋出—訂閱模式。

應用

  • dom的clickfocus等事件
  • 在Vue中的任意元件件通訊事件匯流排EventBus
  • 含有訂閱功能的系統中,如新聞app中訂閱了報紙新聞;

原始碼

簡單版

/**
 * 釋出訂閱模式
 */
class PublishSubscribePattern {
    constructor() {
        // 訊息對映
        this.msgMap = {};
    }
    // 釋出
    publish(name, param) {
        const msg = this.msgMap[name];
        if (msg) {
            msg.subscribes.forEach(subscribe => {
                subscribe.callback(param);
            });
        } else {
            console.log('無人訂閱此訊息:', name, param);
        }
    }
    // 訂閱
    subscribe(name, callback) {
        const msg = this.msgMap[name];
        if (msg) {
            msg.subscribes.push({callback});
        } else {
            this.msgMap[name] = {
                name,
                subscribes: [{callback}]
            }
        }
    }
}
  • 使用
const event = new PublishSubscribePattern();
event.publish('news', 'this is news 1');
event.subscribe('news', (param) => {
    console.log('get news:', param);
});
event.publish('news', 'this is news 2');

帶釋出者版本

訂閱者希望只訂閱某一個釋出者釋出的訊息

  • 原始碼
/**
 * 釋出訂閱模式
 */
class PublishSubscribePattern {
    constructor() {
        // 訊息對映
        this.msgMap = {};
    }
    // 釋出
    publish({name, param, publisher}) {
        const msg = this.msgMap[name];
        if (msg) {
            if (!publisher)  {
                throw new Error('未註冊釋出人:' + name);
            } else if (publisher === 'all') {
                msg.subscribes.forEach(e => e.callback(param));
            } else {
                let beAccept = false;
                msg.subscribes.forEach(e => {
                    if (e.publisher === publisher) {
                        beAccept = true;
                        e.callback(param);
                    }
                });
                if (!beAccept) {
                    console.log('無人訂閱你的訊息:', name, param);
                }
            }
        } else {
            console.log('無人訂閱此訊息:', name, param);
        }
    }
    // 訂閱
    subscribe({name, publisher}, callback) {
        const msg = this.msgMap[name];
        if (msg) {
            msg.subscribes.push({
                publisher,
                callback
            });
        } else {
            this.msgMap[name] = {
                name,
                subscribes: [{
                    publisher,
                    callback
                }]
            }
        }
    }
}
  • 使用
const event = new PublishSubscribePattern();
event.publish({name: 'news', param: 'this is news 1', publisher: 'weather'});

event.subscribe({name: 'news', publisher: 'weather'}, (param) => {
    console.log(`get news from weather:`, param);
});

event.publish({name: 'news', param: 'this is news 2', publisher: 'weather'});
event.publish({name: 'news', param: 'this is news of newspaper', publisher: 'newspaper'});

/*
無人訂閱此訊息: news this is news 1
get news from weather: this is news 2
無人訂閱你的訊息: news this is news of newspaper
*/
複製程式碼

複雜版

複雜版主要實現以下功能:

  • 取消訂閱功能

  • 訂閱者希望只訂閱某一個釋出者釋出的訊息

  • 訂閱者希望獲取到在訂閱之前釋出的歷史訊息

  • 訂閱者希望檢視自己訂閱了哪些訊息

  • 釋出者希望檢視自己釋出過哪些訊息

  • 程式碼

/**
 * 釋出訂閱模式
 */
class Event {
    constructor() {
        // 釋出者對映
        this.publisherMap = {};
        // 訂閱者對映
        this.subscriberMap = {};
    }
    // 註冊
    registration(username, type = 'all') {
        if (type === 'publisher') {
            this.publisherMap[username] = {
                publisher: username,
                msgMap: {}
            };
            return this.publisherMap[username];
        } else if (type === 'subscriber') {
            this.subscriberMap[username] = {
                subscriber: username,
                msgMap: {}
            };
            return this.subscriberMap[username];
        } else if (type === 'all') {
            this.publisherMap[username] = {
                publisher: username,
                msgMap: {}
            };
            this.subscriberMap[username] = {
                subscriber: username,
                msgMap: {}
            };
        }
    }
    // 釋出
    publish({name, param, publisher}) {
        const publisherObj = this.publisherMap[publisher];
        if (!publisherObj)  {
            throw new Error('未註冊釋出人:' + name);
        } else {
            const historyRecord = {
                name,
                param,
                publisher,
                time: Date.now()
            };
            const msg = publisherObj.msgMap[name];
            if (msg) {
                let beAccept = false;
                msg.subscribes.forEach(e => {
                    if (e.publisher === publisher) {
                        beAccept = true;
                        e.callback(param, {name, param, publisher});
                        console.log(e.subscriber, '收到了', e.publisher, '釋出的訊息', name, param);
                    }
                });
                if (!beAccept) {
                    console.log('無人訂閱你的訊息:', name, param);
                }
                msg.history.push(historyRecord);
            } else {
                publisherObj.msgMap[name] = {
                    name,
                    publisher: publisher,
                    subscribes: [],
                    history: [historyRecord]
                };
                console.log('釋出者', publisher, '註冊訊息:', name, param);
            }
        }
    }
    // 訂閱
    subscribe({name, publisher, subscriber, receiveHistoryMsg}, callback) {
        const publisherObj = this.publisherMap[publisher];
        if (subscriber) {
            const subscriberObj = this.subscriberMap[subscriber];
            if (subscriberObj) {
                subscriberObj.msgMap[name] = {
                    name,
                    publisher,
                    subscriber: subscriber,
                    callback,
                    time: Date.now()
                };
            }
        }
        if (publisherObj) {
            const msg = publisherObj.msgMap[name];
            if (msg) {
                msg.subscribes.push({
                    publisher,
                    subscriber,
                    callback
                });
                console.log(subscriber || '遊客', '訂閱了', publisher, '的訊息:', name);
                if (receiveHistoryMsg === true) {
                    msg.history.forEach(e => callback(e.param, e)); 
                }
            } else {
                console.log('釋出者', publisher, '未註冊過此訊息:', name);
            }
        } else {
            console.log('釋出者未註冊:', publisher);
        }
    }
    // 取消訂閱
    unsubscribe({name, publisher, subscriber}) {
        const publisherObj = this.publisherMap[publisher];
        if (subscriber) {
            const subscriberObj = this.subscriberMap[subscriber];
            if (subscriberObj) {
                delete subscriberObj.msgMap[name];
            }
        }
        if (publisherObj) {
            const msg = publisherObj.msgMap[name];
            if (msg) {
                msg.subscribes = msg.subscribes.filter(e => !(e.publisher === publisher && msg.name === name));
            } else {
                console.log('釋出者', publisher, '未註冊過此訊息:', name);
            }
        } else {
            console.log('釋出者未註冊:', publisher);
        }
    }
    // 獲取釋出歷史訊息
    getPublishHistory(publisher, name) {
        return this.publisherMap[publisher].msgMap[name].history;
    }
    getSubscribeMsg(subscriber) {
        return this.subscriberMap[subscriber].msgMap;
    }
}
  • 使用
// 直接使用Event實現釋出訂閱功能
const event = new Event();

const publisher = 'A';

const subscriber = 'B';

event.registration(publisher);
event.registration(subscriber);

const name = 'news';

const param = '一條訊息a';

event.publish({name, publisher, param});

event.subscribe({name, publisher, subscriber, receiveHistoryMsg: true}, (param, e) => {
    console.log(`---- 接收訊息from:`, param, e);
});

event.publish({name, publisher, param: '一條訊息b'});

console.log('訂閱的訊息', event.getSubscribeMsg(subscriber));

event.unsubscribe({name, publisher, subscriber});

event.publish({name, publisher, param: '一條訊息c'});

console.log('釋出歷史', event.getPublishHistory(publisher, name));

/*
釋出者 A 註冊訊息: news 一條訊息a
B 訂閱了 A 的訊息: news
---- 接收訊息from: 一條訊息a { name: 'news',
  param: '一條訊息a',
  publisher: 'A',
  time: 1603011782573 }
---- 接收訊息from: 一條訊息b { name: 'news', param: '一條訊息b', publisher: 'A' }
B 收到了 A 釋出的訊息 news 一條訊息b
訂閱的訊息 { news:
   { name: 'news',
     publisher: 'A',
     subscriber: 'B',
     callback: [Function],
     time: 1603011782575 } }
無人訂閱你的訊息: news 一條訊息c
釋出歷史 [ { name: 'news',
    param: '一條訊息a',
    publisher: 'A',
    time: 1603011782573 },
  { name: 'news',
    param: '一條訊息b',
    publisher: 'A',
    time: 1603011782577 },
  { name: 'news',
    param: '一條訊息c',
    publisher: 'A',
    time: 1603011782578 } ]
*/

使用介面卡類

  • 程式碼
/**
 * 代理類,遮蔽重複設定釋出者、訂閱者
 */
class Factory {
    constructor(username, type) {
        this.username = username;
        this.type = type;
        this._event = new Event();
        this._event.registration(username, type || 'all');
    }
    // 釋出
    publish(param) {
        return this._event.publish(Object.assign({}, param, {publisher: this.username}))
    }
    // 訂閱
    subscribe(param, callback) {
        return this._event.subscribe(Object.assign({}, param, {subscriber: this.username}), callback);
    }
    // 取消訂閱
    unsubscribe(param) {
        return this._event.unsubscribe(Object.assign({}, param, {subscriber: this.username}));
    }
    // 獲取歷史釋出訊息
    getPublishHistory(name) {
        return this._event.getPublishHistory(this.username, name);
    }
    // 獲取訂閱的訊息列表
    getSubscribeMsg() {
        return this._event.getSubscribeMsg(this.username);
    }

}

  • 使用
// 使用介面卡封裝
const publisherA = 'A';
const subscriberB = 'B';
const publisher = new Factory(publisherA, 'publisher');
const subscriber = new Factory(subscriberB, 'subscriber');

const name = '新聞';

publisher.publish({name, param: 'this is news 1'});

subscriber.subscribe({name, publisher: publisherA, receiveHistoryMsg: true}, (param) => {
    console.log(`---- get news from ${publisherA}:`, param);
});
console.log('訂閱的訊息', subscriber.getSubscribeMsg());

publisher.publish({name, param: 'this is news 2'});

publisher.publish({name, param: 'this is news of newspaper'});

console.log('釋出歷史', publisher.getPublishHistory(name));

/*
釋出者 A 註冊訊息: 新聞 this is news 1
釋出者未註冊: A
訂閱的訊息 { '新聞':
   { name: '新聞',
     publisher: 'A',
     subscriber: 'B',
     callback: [Function],
     time: 1603012329816 } }
無人訂閱你的訊息: 新聞 this is news 2
無人訂閱你的訊息: 新聞 this is news of newspaper
釋出歷史 [ { name: '新聞',
    param: 'this is news 1',
    publisher: 'A',
    time: 1603012329813 },
  { name: '新聞',
    param: 'this is news 2',
    publisher: 'A',
    time: 1603012329819 },
  { name: '新聞',
    param: 'this is news of newspaper',
    publisher: 'A',
    time: 1603012329819 } ]
*/

中介者模式(Mediator Pattern)

中介者模式的作用就是解除物件與物件之間的緊耦合關係。

介紹

增加一箇中介者物件後,所有的相關物件都通過中介者物件來通訊,而不是互相引用,所以當一個物件發生改變時,只需要通知 中介者物件即可。中介者使各物件之間耦合鬆散,而且可以獨立地改變它們之間的互動。中介者模式使網狀的多對多關係變成了相對簡單的一對多關係。

中介者模式是迎合最少知識原則(迪米特法則)的一種實現。是指一個物件應 該儘可能少地瞭解另外的物件(類似不和陌生人說話)。

舉例說明

用一個小遊戲說明中介者模式的用處。

遊戲規則:兩組選手進行對戰,其中一個玩家死亡的時候遊戲便結束, 同時通知它的對手勝利。

普通實現

var players = [];
//接著我們再來編寫Hero這個函式;程式碼如下:

var players = []; // 定義一個數組 儲存所有的玩家
function Hero(name,teamColor) {
    this.friends = [];    //儲存隊友列表
    this.enemies = [];    // 儲存敵人列表
    this.state = 'live';  // 玩家狀態
    this.name = name;     // 角色名字
    this.teamColor = teamColor; // 隊伍的顏色
}
Hero.prototype.win = function(){
    console.log("win:" + this.name);
};
Hero.prototype.lose = function(){
    console.log("lose:" + this.name);
};
Hero.prototype.die = function(){
    // 所有隊友死亡情況 預設都是活著的
    var all_dead = true;
    this.state = 'dead'; // 設定玩家狀態為死亡
    for(var i = 0,ilen = this.friends.length; i < ilen; i+=1) {
        // 遍歷,如果還有一個隊友沒有死亡的話,則遊戲還未結束
        if(this.friends[i].state !== 'dead') {
            all_dead = false; 
            break;
        }
    }
    if(all_dead) {
        this.lose();  // 隊友全部死亡,遊戲結束
        // 迴圈 通知所有的玩家 遊戲失敗
        for(var j = 0,jlen = this.friends.length; j < jlen; j+=1) {
            this.friends[j].lose();
        }
        // 通知所有敵人遊戲勝利
        for(var j = 0,jlen = this.enemies.length; j < jlen; j+=1) {
            this.enemies[j].win();
        }
    }
}
// 定義一個工廠類來建立玩家 
var heroFactory = function(name,teamColor) {
    var newPlayer = new Hero(name,teamColor);
    for(var i = 0,ilen = players.length; i < ilen; i+=1) {
        // 如果是同一隊的玩家
        if(players[i].teamColor === newPlayer.teamColor) {
            // 相互新增隊友列表
            players[i].friends.push(newPlayer);
            newPlayer.friends.push(players[i]);
        }else {
            // 相互新增到敵人列表
            players[i].enemies.push(newPlayer);
            newPlayer.enemies.push(players[i]);
        }
    }
    players.push(newPlayer);
    return newPlayer;
};
        // 紅隊
var p1 = heroFactory("aa",'red'),
    p2 = heroFactory("bb",'red'),
    p3 = heroFactory("cc",'red'),
    p4 = heroFactory("dd",'red');

// 藍隊
var p5 = heroFactory("ee",'blue'),
    p6 = heroFactory("ff",'blue'),
    p7 = heroFactory("gg",'blue'),
    p8 = heroFactory("hh",'blue');
// 讓紅隊玩家全部死亡
p1.die();
p2.die();
p3.die();
p4.die();
// lose:dd lose:aa lose:bb lose:cc
// win:ee win:ff win:gg win:hh
複製程式碼

中介者模式實現

玩家與玩家之間的耦合程式碼解除,把所有的邏輯操作放在中介者物件裡面進去處理,某個玩家的任何操作不需要去遍歷去通知其他玩家,而只是需要給中介者傳送一個訊息即可,中介者接受到該訊息後進行處理,處理完訊息之後會把處理結果反饋給其他的玩家物件。

var players = []; // 定義一個數組 儲存所有的玩家
function Hero(name,teamColor) {
    this.state = 'live';  // 玩家狀態
    this.name = name;     // 角色名字
    this.teamColor = teamColor; // 隊伍的顏色
}
Hero.prototype.win = function(){
    // 贏了
    console.log("win:" + this.name);
};
Hero.prototype.lose = function(){
    // 輸了
    console.log("lose:" + this.name);
};
// 死亡
Hero.prototype.die = function(){
    this.state = 'dead';
    // 給中介者傳送訊息,玩家死亡
    playerDirector.ReceiveMessage('playerDead',this);
}
// 移除玩家
Hero.prototype.remove = function(){
    // 給中介者傳送一個訊息,移除一個玩家
    playerDirector.ReceiveMessage('removePlayer',this);
};
// 玩家換隊
Hero.prototype.changeTeam = function(color) {
    // 給中介者傳送一個訊息,玩家換隊
    playerDirector.ReceiveMessage('changeTeam',this,color);
};
// 定義一個工廠類來建立玩家 
var heroFactory = function(name,teamColor) {
    // 建立一個新的玩家物件
    var newHero = new Hero(name,teamColor);
    // 給中介者傳送訊息,新增玩家
    playerDirector.ReceiveMessage('addPlayer',newHero);
    return newHero;
};
var playerDirector = (function(){
    var players = {},  // 儲存所有的玩家
        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;
        }
    }
    // 如果all_dead 為true的話 說明全部死亡
    if(all_dead) {
        for(var i = 0, player; player = teamPlayers[i++]; ) {
            // 本隊所有玩家lose
            player.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 ReceiveMessage = function(){
    // arguments的第一個引數為訊息名稱 獲取第一個引數
    var message = Array.prototype.shift.call(arguments);
    operations[message].apply(this,arguments);
};
return {
    ReceiveMessage : ReceiveMessage
};
})();
// 紅隊
var p1 = heroFactory("aa",'red'),
    p2 = heroFactory("bb",'red'),
    p3 = heroFactory("cc",'red'),
        p4 = heroFactory("dd",'red');

    // 藍隊
    var p5 = heroFactory("ee",'blue'),
        p6 = heroFactory("ff",'blue'),
        p7 = heroFactory("gg",'blue'),
        p8 = heroFactory("hh",'blue');
    // 讓紅隊玩家全部死亡
    p1.die();
    p2.die();
    p3.die();
    p4.die();
    // lose:aa lose:bb lose:cc lose:dd 
   // win:ee win:ff win:gg win:hh
複製程式碼

策略模式(Strategy Pattern)

要實現某一個功能有多種方案可以選擇,定義一系列的演算法,把它們一個個封裝起來,並且使它們可以相互替換,這就是策略模式。

舉例

  • 表單校驗:執行校驗規則校驗規則配置分開;
  • 前端動畫類:將渲染動畫動畫配置以及動畫控制分開

策略模式演示:表單校驗

// 校驗方法&規則配置
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; // 阻止表單提交
    }
};
複製程式碼

優點

策略模式是一種常用且有效的設計模式,總結一下策略模式的一些優點:

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

缺點

策略模式也有一些缺點,但這些缺點並不嚴重:

  • 使用策略模式會在程式中增加許多策略類或者策略物件,但實際上這比把它們負責的 邏輯堆砌在 Context 中要好。
  • 要使用策略模式,必須瞭解所有的 strategy,必須瞭解各個 strategy 之間的不同點, 這樣才能選擇一個合適的 strategy。比如,我們要選擇一種合適的旅遊出行路線,必須先了解選 擇飛機、火車、自行車等方案的細節。此時 strategy 要向客戶暴露它的所有實現,這是違反最少 知識原則的。

總結

不僅是演算法,業務規則指向的目標一致,並且可以被替換使用,就也可以用策略模式來封裝它們。

“在函式作為一等物件的語言中,策略模式是隱形的。 strategy 就是值為函式的變數。”

相對傳統面嚮物件語言的方式實現策略模式,使用 JavaScript 語言的策略模式,策略類往往被函式所代替,這時策略模式就 成為一種“隱形”的模式。

代理模式(Proxy Pattern)

為其他物件提供一種代理以控制對這個物件的訪問。

常用的代理模式變種有以下幾種:

  • 保護代理
  • 虛擬代理
  • 快取代理

1. 保護代理

保護代理用於控制不同許可權的物件對目標物件的訪問

  • 舉例
class Car {
    drive() {
        return "driving";
    };
}

class CarProxy {
    constructor(driver) {
        this.driver = driver;
    }
    drive() {
        // 保護代理,僅18歲才能開車
        return (this.driver.age < 18) ? "too young to drive" : new Car().drive();
    };
}
複製程式碼

2. 虛擬代理

虛擬代理可應用於:圖片懶載入惰性載入合併http請求

  • 舉例:圖片懶載入
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>圖片懶載入</title>
    <style>
        img {
            display: block;
            width: 400px;
            height: 300px;
            margin-bottom: 200px;
        }
    </style>
</head>
<body>
    <img data-src="./images/1.jpg" alt="">
    <img data-src="./images/2.jpg" alt="">
    <img data-src="./images/3.jpg" alt="">
    <img data-src="./images/4.jpg" alt="">
</body>
<script>
    var imgs = document.querySelectorAll('img');
    //offsetTop是元素與offsetParent的距離,迴圈獲取直到頁面頂部
    function getTop(e) {
    var T = e.offsetTop;
        while(e = e.offsetParent) {
            T += e.offsetTop;
        }
        return T;
    }
    function lazyLoad(imgs) {
        var H = document.documentElement.clientHeight;//獲取可視區域高度
        var S = document.documentElement.scrollTop || document.body.scrollTop;
        for (var i = 0; i < imgs.length; i++) {
            if (H + S > getTop(imgs[i])) {
                imgs[i].src = imgs[i].getAttribute('data-src');
            }
        }
    }
    window.onload = window.onscroll = function () { //onscroll()在滾動條滾動的時候觸發
        lazyLoad(imgs);
    }
</script>
</html>
複製程式碼

3. 快取代理

快取代理可應用於:快取ajax非同步請求資料計算乘積

  • 舉例:快取ajax請求資料
const getData = (function() {
    const cache = {};
    return function(url) {
        if (cache[url]) {
            return Promise.resolve(cache[url]);
        }
        return $.ajax.get(url).then((res) => {
            cache[url] = res;
            return res;
        }).catch(err => console.error(err))
    }
})();

getData('/getData'); // 發起http請求
getData('/getData'); // 返回快取資料
複製程式碼

總結

代理模式包括許多小分類,在 JavaScript 開發中最常用的是虛擬代理和快取代理。雖然代理模式非常有用,但我們在編寫業務程式碼的時候,往往不需要去預先猜測是否需要使用代理模式。當真正發現不方便直接訪問某個物件的時候,再編寫代理也不遲。

迭代器模式(Iterator Pattern)

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

迭代器模式可以把迭代的過程從業務邏輯中分離出來,在使用迭代器模式之後,即使不關心物件的內部構造,也可以按順序訪問其中的每個元素。

例如 Javascript中的forEachmapsome等;

迭代器可分為一下兩種:

  • 內部迭代器
  • 外部迭代器

內部迭代器

內部已經定義好了迭代規則,它完全接手整個迭代過程,外部只需要一次初始呼叫。

  • 例如:
function each(ary, callback) {
    for (let i = 0; i < ary.length; i++){
        callback.call(ary[i], i, ary[i]);
    }
}
each([1, 2, 3], (i, n) => alert([i, n]));
複製程式碼

總結:內部迭代器呼叫方式簡答,但它的適用面相對較窄。

外部迭代器

必須顯式地請求迭代下一個元素。

外部迭代器增加了一些呼叫的複雜度,但相對也增強了迭代器的靈活性,我們可以手工控制迭代的過程或者順序。

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
    }
};
複製程式碼

總結:外部迭代器雖然呼叫方式相對複雜,但它的適用面更廣,也能滿足更多變的需求。內部迭代器和外部迭代器在實際生產中沒有優劣之分,究竟使用哪個要根據需求場景而定。

迭代類陣列物件和字面量物件

迭代器模式不僅可以迭代陣列,還可以迭代一些類陣列的物件。

無論是內部迭代器還是外部迭代器,只要被迭代的聚合物件擁有 length 屬性而且可以用下標訪問,那它就可以被迭代。

例如:arguments{'0': 'a', '1': 'b'}

// 迭代器支援類陣列和物件的遍歷
function each(obj, callback) {
    var value, i = 0, length = obj.length, isArray = isArraylike( obj );
    if ( isArray ) {
        for ( ; i < length; i++ ) {
            value = callback.call( obj[ i ], i, obj[ i ] );
            if ( value === false ) {
                break;
            }
        }
    } else {
        // 迭代object 物件
        for ( i in obj ) {
            value = callback.call( obj[ i ], i, obj[ i ] );
            if ( value === false ) {
                break;
            }
        }
    }
    return obj;
};
複製程式碼

倒序迭代器

從尾到頭的遍歷陣列。

中止迭代器

在遍歷過程中,如果滿足某種條件可以終止迭代。

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

迭代器模式的應用舉例

  • nodejs的express框架的中介軟體思想,處理請求用到的next()方法就是迭代器模式。
  • ECMAScript 6 的Iterator(遍歷器)和Generator非同步程式設計中使用了next()

介面卡模式(Adapter Pattern)

介面卡模式的作用是解決兩個介面/方法間的介面不相容的問題。

作為兩個不相容的介面之間的橋樑,就是新增一個包裝類,對新的介面進行包裝以適應舊程式碼的呼叫,避免修改介面和呼叫程式碼。

示例

// 1\. 方法適配 *******************************
const A = {
    show() {
       console.log('visible');
    }
}
const B = {
    display() {
       console.log('visible');
    }
}
// 不使用介面卡
A.show();
B.display();

// 使用介面卡
const C = {
    show() {
       B.display();
    }
}
A.show();
C.show();

// 2\. 介面適配 *******************************
const data1 = {name: 'alan'};
const data2 = {username: 'tom'};
function sayName(param) {
    console.log(param.name);
}
function adapter(param) {
    return {name: param.username}
}
sayName(data1);
sayName(adapter(data2));
複製程式碼

相似模式之間的差異

有一些模式跟介面卡模式的 結構非常相似,比如裝飾者模式、代理模式和外觀模式。

這幾種模式都屬於“包 裝模式”,都是由一個物件來包裝另一個物件。區別它們的關鍵仍然是模式的意圖。

  • 介面卡模式主要用來解決兩個已有介面之間不匹配的問題,它不考慮這些介面是怎樣實 現的,也不考慮它們將來可能會如何演化。介面卡模式不需要改變已有的介面,就能夠 使它們協同作用。
  • 裝飾者模式和代理模式也不會改變原有物件的介面,但裝飾者模式的作用是為了給物件 增加功能。裝飾者模式常常形成一條長的裝飾鏈,而介面卡模式通常只包裝一次。代理 模式是為了控制對物件的訪問,通常也只包裝一次。
  • 外觀模式的作用倒是和介面卡比較相似,有人把外觀模式看成一組物件的介面卡,但外 觀模式最顯著的特點是定義了一個新的介面。

總結

介面卡模式是作為一箇中間的橋樑,使原本有差異的介面變成需要的標準,能夠滿足現有介面的需要,而不需要去修改現有的原始碼。

命令模式(Command Pattern)

命令模式將呼叫者和執行者之間分開,通過命令來對映各種操作,從而達到鬆耦合的目的。

命令模式的由來,其實是回撥(callback)函式的一個面向物件的替代品。JavaScript 作為將函式作為一等物件的語言,跟策略模式一樣,命令模式也早已融入到了 JavaScript 語言之中。

應用場景

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

命令模式的呼叫者只需要下達命令,不需要知道命令被執行的具體細節。從而使呼叫者專注於自己的主流程。

特點

  • 鬆耦合:將請求呼叫者和請求接收者鬆耦合
  • 生命週期
  • 支援撤銷
  • 支援排隊

舉例

命令模式實現向左、向右、及撤銷操作

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>命令模式</title>
    <style>
        #app{width: 100%;max-width: 400px;margin: 0 auto;}
        .container{width: 100%;background-color: #ddd;margin-top: 40px;}
        .box{width: 25px;height: 15px;background-color: #ff5b5c;}
    </style>
</head>
<body>
    <div id="app">
        <h1>命令模式</h1>
        <button id="left">向左</button>
        <button id="right">向右</button>
        <button id="turnback">撤銷</button>
        <div style="margin-top: 20px"><strong> history: </strong><span id="history" ></span></div>
        <div class="container">
            <div id="box" class="box"></div>
        </div>
    </div>
<script>

const step = 25;
const right = document.getElementById('right');
const left = document.getElementById('left');
const turnback = document.getElementById('turnback');
const historyLog = document.getElementById('history');

class Box{
    constructor({pos = 0, id}) {
        this.pos = pos;
        this.$box = document.getElementById(id);
        this.update();
    }
    moveTo(pos) {
        this.pos = pos;
        this.update();
    }
    update() {
        this.$box.style.marginLeft = this.pos + 'px';
    }
}

class Command {
    constructor(box) {
        this.pos = box.pos || 0;
        this.box = box;
        this.history = [this.pos];
        historyLog.innerText = this.history;
    }
    run(dis) {
        this.pos = this.pos + dis;
        this.box.moveTo(this.pos);
        this.history.push(this.pos);
        historyLog.innerText = this.history;
        console.log('run:', this.pos, this.history);
    }
    cancel() {
        if (this.history.length === 0) {alert('回到起點');return}
        const record = this.history.pop();
        this.pos = this.history[this.history.length - 1];
        this.box.moveTo(this.pos);
        historyLog.innerText = this.history;
        console.log('cancel:', record, this.history);
    }
}

const box = new Box({id: 'box', pos: 2 * step});
const command = new Command(box);

right.addEventListener('click', () => command.run(step));
left.addEventListener('click', () => command.run(-step));
turnback.addEventListener('click', () => command.cancel());

</script>
</body>
</html>
複製程式碼

組合模式(Composite Pattern)

組合模式(Composite Pattern),又叫部分整體模式,是用於把一組相似的物件當作一個單一的物件。組合模式依據樹形結構來組合物件,用來表示部分以及整體層次。這種型別的設計模式屬於結構型模式,它建立了物件組的樹形結構。

當我們想執行一個巨集大的任務時,這個任務可以被細分為多層結構,組合模式可以讓我們只發布一次執行命令,就能完成整個複雜的任務,遮蔽層級關係差異化問題

注意

  1. 組合模式不是父子關係,它們能夠合作的關鍵是擁有相同的介面;
  2. 對葉物件操作的一致性,要對每個目標物件都實行同樣的操作;
  3. 可用中介者模式處理雙向對映關係,例如一個子節點同時在不同的父節點中存在(組織架構);
  4. 可用職責鏈模式提高組合模式效能,通過設定鏈條避免每次都遍歷整個樹;

應用場景

組合模式應樹形結構而生,所以組合模式的使用場景就是出現樹形結構的地方。

  • 命令分發:只需要通過請求樹的最頂層物件,便能對整棵樹做統一的操作。在組合模式中增加和刪除樹的節點非常方便,並且符合開放-封閉原則
  • 統一處理:統一對待樹中的所有物件,忽略組合物件和葉物件的區別;

比如:檔案目錄顯示,多級目錄呈現等樹形結構資料的操作。

舉例:檔案系統操作

// 資料夾
var Folder = function( name ){
    this.name = name;
    this.parent = null; //增加this.parent 屬性
    this.files = [];
};

Folder.prototype.add = function( file ){
    file.parent = this; //設定父物件
    this.files.push( file );
};

Folder.prototype.scan = function(){
    console.log( '開始掃描資料夾: ' + this.name );
    for ( var i = 0, file, files = this.files; file = files[ i++ ]; ){
        file.scan();
    }
};

Folder.prototype.remove = function(){
    if ( !this.parent ){ //根節點或者樹外的遊離節點
        return;
    }
    for ( var files = this.parent.files, l = files.length - 1; l >=0; l-- ){
        var file = files[ l ];
        if ( file === this ){
            files.splice( l, 1 );
        }
    }
};
// 檔案
var File = function( name ){
    this.name = name;
    this.parent = null;
};

File.prototype.add = function(){
    throw new Error( '不能新增在檔案下面' );
};

File.prototype.scan = function(){
    console.log( '開始掃描檔案: ' + this.name );
};

File.prototype.remove = function(){
    if ( !this.parent ){ //根節點或者樹外的遊離節點
        return;
    }

    for ( var files = this.parent.files, l = files.length - 1; l >=0; l-- ){
        var file = files[ l ];
        if ( file === this ){
            files.splice( l, 1 );
        }
    }
};

var folder = new Folder( '學習資料' );
var folder1 = new Folder( 'JavaScript' );
var file1 = new Folder ( '深入淺出Node.js' );

folder1.add( new File( 'JavaScript 高階程式設計' ) );
folder.add( folder1 );
folder.add( file1 );
folder1.remove(); //移除資料夾
folder.scan();
複製程式碼

總結

組合模式可以讓我們使用樹形方式創 建物件的結構。我們可以把相同的操作應用在組合物件和單個物件上。在大多數情況下,我們都 可以忽略掉組合物件和單個物件之間的差別,從而用一致的方式來處理它們。

模板方法模式(Template Method)

模板方法模式是一種通過封裝變化提高系統擴充套件性的設計模式。

在傳統的面嚮物件語言中,一個運用了模板方法模式的程式中,子類的方法種類和執行順序都是不變的,所以我們把 這部分邏輯抽象到父類的模板方法裡面。而子類的方法具體怎麼實現則是可變的,於是我們把這 部分變化的邏輯封裝到子類中。通過增加新的子類,我們便能給系統增加新的功能,並不需要改 動抽象父類以及其他子類,這也是符合開放-封閉原則的。

應用場景

假如我們有一些平行的子類,各個子類之間有一些相同的行為,也有一些不同的行為。如果相同和不同的行為都混合在各個子類的實現中,說明這些相同的行為會在各個子類中重複出現。 但實際上,相同的行為可以被搬移到另外一個單一的地方,模板方法模式就是為解決這個問題而生的。在模板方法模式中,子類實現中的相同部分被上移到父類中,而將不同的部分留待子類來實現。這也很好地體現了泛化的思想。

組成

模板方法模式由兩部分結構組成:

  • 抽象父類:通常在抽象父類中封裝了子類的演算法框架,包括實現一些公共方法以及封裝子類中所有方法的執行方式(比如執行順序、條件執行等)。
  • 具體的實現子類:子類通過繼承這個抽象類,也繼承了整個演算法結構,並且可以選擇重寫父類的方法。

示例

模板方法模式通常通過繼承來實現,在 JavaScript 開發中用到繼承的場景其實並不是很多,很多時候我們都喜歡用 mix-in 的方式給物件擴充套件屬性。

抽象類定義執行方法的方式(比如執行順序、條件執行等),它的子類可以按需要重寫被執行的方法,核心是抽象類。

class Tax {
  calc(value) {
    if (value >= 1000)
      value = this.overThousand(value);

    return this.complementaryFee(value);
  }
  complementaryFee(value) {
    return value + 10;
  }
}

class Tax1 extends Tax {
  constructor() {
    super();
  }
  overThousand(value) {
    return value * 1.1;
  }
}
複製程式碼

享元模式(Flyweight Pattern)

享元模式(Flyweight Pattern)主要用於減少建立物件的數量,以減少記憶體佔用和提高效能。

享元模式屬於結構型模式,它提供了減少物件數量從而改善應用所需的物件結構的方式。

適應場景

解決的問題:在有大量物件時,有可能會造成記憶體溢位,我們把其中共同的部分抽象出來,如果有相同的業務請求,直接返回在記憶體中已有的物件,避免重新建立。

使用注意:使用了享元模式之後,我們需要分別多維護一個 factory 物件和一個 manager 物件,在大部分不必要使用享元模式的環境下,這些開銷是可以避免的。

因此我們可以在以下場景中使用享元模式:

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

內部狀態與外部狀態

享元模式要求將物件的屬性劃分為內部狀態外部狀態(狀態在這裡通常指屬性)。享元模式的目標是儘量減少共享物件的數量,那麼如何劃分內部狀態和外部狀態呢?

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

把所有內部狀態相同的物件都指定為同一個共享的物件。而外部狀態 可以從物件身上剝離出來,並儲存在外部。

剝離了外部狀態的物件成為共享物件,外部狀態在必要時被傳入共享物件來組裝成一個完整 的物件。雖然組裝外部狀態成為一個完整物件的過程需要花費一定的時間,但卻可以大大減少系 統中的物件數量,相比之下,這點時間或許是微不足道的。因此,享元模式是一種用時間換空間的優化模式。

使用享元模式的關鍵是如何區別內部狀態外部狀態

舉例

  • 程式碼關鍵點:用 HashMap 物件池儲存這些物件。
// 享元模式,物件池快取物件
class colorFactory {
  constructor(name) {
    this.colors = {};
  }
  create(name) {
    let color = this.colors[name];
    if (color) return color;
    this.colors[name] = new Color(name);
    return this.colors[name];
  }
};
複製程式碼

優缺點

優點:大大減少物件的建立,降低系統的記憶體,使效率提高。

缺點:提高了系統的複雜度,需要分離出外部狀態和內部狀態,而且外部狀態具有固有化的性質,不應該隨著內部狀態的變化而變化,否則會造成系統的混亂。

模職責鏈模式 (Chain of Responsibility Pattern)

責任鏈模式(Chain of Responsibility Pattern)為請求建立了一個接收者物件的鏈。使多個物件都有機會處理請求,從而避免請求的傳送者和接收者之間的耦合關係。

在這種模式中,通常每個接收者都包含對另一個接收者的引用。如果一個物件不能處理該請求,那麼它會把相同的請求傳給下一個接收者,依此類推,直到有一個物件處理它為止。

這種型別的設計模式屬於行為型模式

適用場景

職責鏈上的處理者負責處理請求,客戶只需要將請求傳送到職責鏈上即可,無須關心請求的處理細節和請求的傳遞,所以職責鏈將請求的傳送者和請求的處理者解耦了。

  1. 有多個物件可以處理同一個請求,具體哪個物件處理該請求由執行時刻自動確定。
  2. 在不明確指定接收者的情況下,向多個物件中的一個提交一個請求。
  3. 可動態指定一組物件處理請求。

應用示例

JS 中的事件冒泡 express、koa中介軟體洋蔥模型

  • 實現簡易koa中介軟體【職責鏈模式】
class Middleware {
    constructor() {
        this.middlewares = [];
    }
    use(fn) {
        if(typeof fn !== 'function') {
            throw new Error('Middleware must be function, but get ' + typeof fn);
        } 
        this.middlewares.push(fn);
        return this;
    }
    compose() {
        const middlewares = this.middlewares;
        return dispatch(0);
        function dispatch(index) {
            const middleware = middlewares[index];
            if (!middleware) {return;}
            try{
                const ctx = {};
                const result = middleware(ctx, dispatch.bind(null, index + 1));
                return Promise.resolve(result);
            } catch(err) {
                return Promise.reject(err);
            }
        }
    }
}

const middleware = new Middleware();
middleware.use(async (ctx, next) => {
    console.log(1);
    await next();
    console.log(2);
});
middleware.use(async (ctx, next) => {
    console.log(3);
    await next();
    console.log(4);
});

middleware.compose();// 1 3 4 2
複製程式碼

優缺點

優點:

  1. 降低耦合度。它將請求的傳送者和接收者解耦。
  2. 簡化了物件。使得物件不需要知道鏈的結構。
  3. 增強給物件指派職責的靈活性。通過改變鏈內的成員或者調動它們的次序,允許動態地新增或者刪除責任。
  4. 增加新的請求處理類很方便。

缺點:

  1. 不能保證請求一定被接收。
  2. 系統性能將受到一定影響,而且在進行程式碼除錯時不太方便,可能會造成迴圈呼叫。
  3. 可能不容易觀察執行時的特徵,有礙於除錯。

總結

職責鏈模式可以很好地幫助我們管理程式碼,降低發起請求的物件和處理請求的物件之間的耦合性。職 責鏈中的節點數量和順序是可以自由變化的,我們可以在執行時決定鏈中包含哪些節點。

Javascript 設計模式 - 狀態模式

當控制物件狀態的條件表示式過於複雜時的情況,把狀態的判斷邏輯轉移到表示不同的狀態的一系列類或者方法當中,可以把複雜的邏輯判斷簡單化。

狀態模式允許一個物件在其內部狀態改變時改變它的行為,物件看起來似乎修改了它的類。

這種型別的設計模式屬於行為型模式。

狀態模式的關鍵是區分事物內部的狀態,事物內部狀態的改變往往會帶來事物的行為改變。

在狀態模式中,我們建立表示各種狀態的物件和一個行為隨著狀態物件改變而改變的context物件。

介紹

意圖:允許物件在內部狀態發生改變時改變它的行為,物件看起來好像修改了它的類。

主要解決:物件的行為依賴於它的狀態(屬性),並且可以根據它的狀態改變而改變它的相關行為。

何時使用:程式碼中包含大量與物件狀態有關的條件語句。

如何解決:將各種具體的狀態類抽象出來。

使用場景:

  1. 行為隨狀態改變而改變的場景。
  2. 條件、分支語句的代替者。

注意事項:在行為受狀態約束的時候使用狀態模式,而且狀態不超過 5 個。

優缺點

優點:

  1. 封裝了轉換規則。
  2. 列舉可能的狀態,在列舉狀態之前需要確定狀態種類。
  3. 將所有與某個狀態有關的行為放到一個類中,並且可以方便地增加新的狀態,只需要改變物件狀態即可改變物件的行為。
  4. 允許狀態轉換邏輯與狀態物件合成一體,而不是某一個巨大的條件語句塊。
  5. 可以讓多個環境物件共享一個狀態物件,從而減少系統中物件的個數。

缺點:

  1. 狀態模式的使用必然會增加系統類和物件的個數。
  2. 狀態模式的結構與實現都較為複雜,如果使用不當將導致程式結構和程式碼的混亂。
  3. 狀態模式對"開閉原則"的支援並不太好,對於可以切換狀態的狀態模式,增加新的狀態類需要修改那些負責狀態轉換的原始碼,否則無法切換到新增狀態,而且修改某個狀態類的行為也需修改對應類的原始碼。

應用

  • 檔案下載(開始、暫停、完成、失敗等)
  • 遊戲(走動、攻擊、防禦、跌倒、跳躍)
  • 紅綠燈(紅、綠、黃切換)

狀態模式和策略模式的關係

狀態模式和策略模式像一對雙胞胎,它們都封裝了一系列的演算法或者行為,但在意圖上有很大不同,因此它們是兩種迥然不同的模式。

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

它們之間的區別是策略模式中的各個策略類之間是平等又平行的,它們之間沒有任何聯絡, 所以客戶必須熟知這些策略類的作用,以便客戶可以隨時主動切換演算法;而在狀態模式中,狀態 和狀態對應的行為是早已被封裝好的,狀態之間的切換也早被規定完成,“改變行為”這件事情 發生在狀態模式內部。對客戶來說,並不需要了解這些細節。這正是狀態模式的作用所在。

總結

狀態模式可用來優化條件分支語句、含有大量狀態且行為隨狀態改變而改變的場景。一開始不太好理解,但使用得當可以讓程式碼的結構變得非常清晰和可擴充套件。

參考

我平時一直有整理面試題的習慣,有隨時跳出舒適圈的準備,不知不覺整理了229頁了,在這裡分享給大家,有需要的點選這裡免費領取題目+解析PDF

篇幅有限,僅展示部分內容

如果你需要這份完整版的面試題+解析,【點選我】就可以了。

希望大家明年的金三銀四面試順利,拿下自己心儀的offer!