在程序設計中有不少實用的設計模式,而其中大部分語言的實現都是基於「類」。css
在JavaScript中並無類這種概念,JS中的函數屬於一等對象,在JS中定義一個對象很是簡單(var obj = {}),而基於JS中閉包與弱類型等特性,在實現一些設計模式的方式上不同凡響。html
本文基於《JavaScript設計模式與開發實踐》一書,用一些例子總結一下JS常見的設計模式與實現方法。文章略長,自備瓜子板凳~前端
單一職責原則(SRP)web
一個對象或方法只作一件事情。若是一個方法承擔了過多的職責,那麼在需求的變遷過程當中,須要改寫這個方法的可能性就越大。面試
應該把對象或方法劃分紅較小的粒度算法
最少知識原則(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. 定義
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
個人博客即將搬運同步至騰訊雲+社區,邀請你們一同入駐:https://cloud.tencent.com/developer/support-plan?invite_code=2kmd69ge4zeow