1. 程式人生 > >JavaScript中常見的十五種設計模式

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

在程式設計中有很多實用的設計模式,而其中大部分語言的實現都是基於“類”。

在JavaScript中並沒有類這種概念,JS中的函式屬於一等物件,在JS中定義一個物件非常簡單(var obj = {}),而基於JS中閉包與弱型別等特性,在實現一些設計模式的方式上與眾不同。

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

設計原則

單一職責原則(SRP)

一個物件或方法只做一件事情。如果一個方法承擔了過多的職責,那麼在需求的變遷過程中,需要改寫這個方法的可能性就越大。

應該把物件或方法劃分成較小的粒度

最少知識原則(LKP)

一個軟體實體應當 儘可能少地與其他實體發生相互作用 

應當儘量減少物件之間的互動。如果兩個物件之間不必彼此直接通訊,那麼這兩個物件就不要發生直接的 相互聯絡,可以轉交給第三方進行處理

開放-封閉原則(OCP)

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

當需要改變一個程式的功能或者給這個程式增加新功能的時候,可以使用增加程式碼的方式,儘量避免改動程式的原始碼,防止影響原系統的穩定

什麼是設計模式

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

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

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

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

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

一、單例模式

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

複製程式碼

二、策略模式

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. 優缺點

優點

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

缺點

往往策略集會比較多,我們需要事先就瞭解定義好所有的情況

三、代理模式

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

複製程式碼

四、迭代器模式

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

複製程式碼

五、釋出-訂閱模式

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*框架中

缺點

建立訂閱者本身要消耗一定的時間和記憶體,訂閱的處理函式不一定會被執行,駐留記憶體有效能開銷

弱化了物件之間的聯絡,複雜的情況下可能會導致程式難以跟蹤維護和理解

六、命令模式

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

複製程式碼

七、組合模式

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. 優缺點

優點

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

缺點

創建出來的物件長得都差不多,可能會使程式碼不好理解,建立太多的物件對效能也會有一些影響

八、模板方法模式

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();

複製程式碼

九、享元模式

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物件就更接近單例模式了

十、職責鏈模式

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

複製程式碼

由例子可以看到,使用了職責鏈後,由原本的條件分支換成了很多物件,雖然結構更加清晰了,但在一定程度上可能會影響到效能,所以要注意避免過長的職責鏈。

十一、中介者模式

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將更為輕鬆,整體程式碼也更簡潔

最後,雖然中介者做到了對模組和物件的解耦,但有時物件之間的關係並非一定要解耦,強行使用中介者來整合,可能會使程式碼更為繁瑣,需要注意。

十二、裝飾者模式

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(); // 跑步 音樂 數學

複製程式碼

十三、狀態模式

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. 優缺點

優點

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

缺點

多個狀態類,對於效能來說,也是一個缺點,這個缺點可以使用享元模式來做進一步優化

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

十四、介面卡模式

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

複製程式碼

十五、外觀模式

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

複製程式碼

[-_-]眼睛累了吧,注意勞逸結合呀[-_-]