模式是對某情景下,針對某種問題的某種解決方案。而一個設計模式是用來解決一個常常出現的設計問題的經驗方法。這麼說來,每一個模式均可能有着本身的意圖,應用場景,使用方法和使用後果。本文的行文思路和目的皆在於瞭解各個模式的定義,應用場景和用實例說明如何在前端開發中使用。javascript
本文所設計到的概念和實例大多來自《Head First設計模式》和《JavaScript設計模式和開發實踐》二書,前者以生動形象的例子和簡明幽默的句子闡述了何爲設計模式,鑑於JavaScript語言的特殊性,後者以實例說明了在JavaScript中如何應用設計模式,兩本都是我讀後收穫很是大的書。前端
關於模式的分類,是爲了創建起模式之間的關係。本文采用最廣爲人知的分類:建立型、行爲型、結構型來敘述。本文只涉及到部分模式,在以後的學習過程當中,本人還好不斷修改和補充。java
「模式只是指導方針,實際工做中,能夠改變模式來適應實際問題。」程序員
將對象實例化,這類模式都提供一個方法,將客戶從所須要的實例化的對象中解耦。算法
策略模式定義了算法組,分別封裝起來,讓他們之間能夠互相替換,此模式讓算法的變化獨立於使用算法的客戶。編程
要達到某一個目的,根據具體的實際狀況,選擇合適的方法。適合於實現某一個功能有多種方案能夠選擇的情景。segmentfault
策略類的組成:設計模式
一組策略類,策略類封裝了具體的算法,並負責具體的計算過程;緩存
環境類:負責接收客戶的請求,並把請求委託給某一個策略類;安全
一個按不一樣等級計算年終獎的例子
// 策略組 var strategies = { "S": function(salary){ return salary * 4; }, "A": function(salary){ return salary * 3; }, "B":function(salary){ return salary * 2 } }; // 內容組 var calculateBonus = function(level,salary){ return strategies[level](salary); } // 執行 console.log(calculateBonus('S',20000)); // 輸出:80000 console.log(calculateBonus('A',10000)); // 輸出:30000
單件模式確保一個類只有一個實例,並提供一個全局訪問點。
用於建立獨一無二的,只能有一個實例的對象,單件模式給了咱們一個全局的訪問點,和全局變量同樣方便又沒有全局變量的缺點。
沒有公開的構造器,利用延遲實例化的方式來建立單件,這種作法對資源敏感的對象特別重要。
傳統語言的實現:
而對JavaScript而言,並沒有類的概念,所以要實現它的核心,確保只有一個實例並提供全局訪問。可是把全局變量當成單例來使用容易形成命名污染。
防止命名空間污染的方法:
使用命名空間
使用閉包封裝私有變量
JavaScript惰性單例
惰性單例指的是在須要的時候才建立對象單例。
代碼示例:
// 單例模式 var getSingle = function(fn){ var result; return function(){ return result || (result = fn.apply(this,arguments)) } }; var createLoginLayer = function(){ var div = document.createElement('div'); div.innerHTML = '我是登錄窗'; div.style.display = 'none'; document.body.appendChild(div); } var createSingleLoginLayer = getSingle(createLoginLayer);
工廠方法模式定義了一個建立對象的接口,但由子類決定要實例化的類是哪個,工廠方法讓類把實例化推遲到子類。
建立新對象,且該對象須要被被封裝。
工廠模式經過讓子類來決定該建立的對象是什麼,來達到將對象建立的過程封裝的目的。
建立對象的方法使用的是繼承,用於建立一個產品的實例;
提供一個藉口,用於建立相關或依賴對象的家族,而不須要明確指定具體類。
定義一個負責建立一組產品的接口,這個接口內的每個方法都負責建立一個具體產品。抽象工廠的方法一般以工廠方法的方式實現。
建立對象的方法使用的是組合,把一羣相關的產品集合起來,相似於工廠裏有一個個的車間。用於建立一組產品。
類和對象如何交互和分配職責
在一個方法中定義一個算法的骨架,而將一些步驟延遲到子類中。模板方法使得子類能夠在不改變算法結構的狀況下,從新定義算法中的某些步驟。模板就是一個方法,這個方法將算法定義爲一個步驟,其中的任何步驟均可以是抽象的,由子類負責實現。
適用於算法的結構保持不變,同時由子類提供部分實現的狀況。常被架構師用於搭建項目的框架,架構師定好了骨架,程序員繼承了骨架的結構以後,負責往裏面填空。
鉤子是一種被聲明在抽象類中的方法,只有空的或默認的實現。鉤子的存在,可讓子類有能力對算法的不一樣點進行掛鉤。要不要掛鉤,由子類決定(可選)。在容易變化的地方放置鉤子,鉤子能夠有一個默認的實現,可是究竟要不要「掛鉤」,這由子類自行決定。
一個經典的coffee or tea的例子
// 建立抽象父類 var Beverage = function(){}; Beverage.prototype.boilWater = function(){ console.log('把水煮沸'); }; // 三個空方法,由子類實現 Beverage.prototype.brew = function(){}; Beverage.prototype.pourIncup = function(){}; Beverage.prototype.addCondimwnts = function(){}; // 實現順序 Beverage.prototype.init = function(){ this.boilWater(); this.brew(); this.pourInCup(); this.addCondiments(); }; // 實現煮咖啡 var Coffee = function(){}; Coffee.prototype = new Beverage(); Coffee.prototype.brew =function(){ console.log('煮咖啡'); }; Coffee.prototype.pourIncup = function(){ console.log('coffee倒入杯子'); }; Coffee.prototype.addCondiments = function(){ console.log('加糖和牛奶'); }; var coffee = new Coffee(); coffee.init(); // 實現怕茶 var Tea = function(){}; Tea.prototype = new Beverage(); Tea.prototype.brew =function(){ console.log('泡茶'); }; Tea.prototype.pourIncup = function(){ console.log('tea倒入杯子'); }; Tea.prototype.addCondiments = function(){ console.log('加檸檬'); }; var tea = new Tea(); tea.init();
命令模式將請求封裝成對象,以便使用不一樣的請求、隊列或者日誌來參數化其餘對象,命令模式也支持可撤銷的操做。
有時候須要向某些對象發送請求,可是並不知道請求的接受者是誰,也不知道請求的操做是什麼,將‘對象的請求者‘從’命令的執行者’中解耦。使用此模式的優勢還在於,command對象擁有更長的生命週期,能夠在程序運行的任什麼時候刻去調用這個方法。
命令模式將動做和接受者包進對象中。這個對象只暴露出一個execute()方法,當此方法被調用的時候,接受者就會進行這些動做。從外面來看,其它對象不知道究竟哪一個接受者進行了這些動做,只知道若是調用execute()方法,請求目的就達到了。
命令模式的由來,實際上是回調函數的一個面向對象的替代品,命令模式早已融入到了JavaScript語言之中。
// 命令模式 // 具體的命令執行動做(廚師炒菜) var MenuBar = { refresh:function(){ console.log('刷新菜單界面') } } // 傳遞命令(把菜單給廚師) var RefreshMenuBarCommand = function(receiver){ return{ execute:function(){ receiver.refresh(); } } } // 可見的命令(菜單) var setCommand = function(button,command){ button.onclick = function(){ command.execute() } } // 請求命令(點餐) var refreshMenuBarCommand = RefreshMenuBarCommand(MenuBar); // 執行命令(在顧客不可見的狀況下,廚師炒菜) setCommand(button1,refreshMenuBarCommand)
迭代器模式提供一種方法順序訪問一個聚合對象中的各個元素,而又不暴露其內部的表示,有內部迭代器和外部迭代器之分,其中內部迭代器全接手整個迭代過程,外部只須要一次初始調用,而外部迭代器必須顯式的請求下一個元素。
須要順序訪問一個組合內的多個對象的時候使用。
一個對比對象的例子
var Iterator = function(obj){ var current = 0; var next = function(){ current + = 1; }; var isDone = function(){ return current >=obj.length; }; var getCurrItem = function(){ return obj[current]; }; return{ next:next, isDone:isDone, getCurrItem:getCurrItem } } var compare = function(iterator1,iterator2){ while(!iterator1.isDone() && !iterator2.isDone()){ if (iterator1.getCurrItem() !== iterator2.getCurrItem()) { throw new Error('iteraor1和iteraor2不相等'); } iterator1.next(); iterator2.next(); } alert('兩者相等'); } var iterator1 = Iterator([1,2,3]); var iterator2 = Iterator([1,2,3]); compare(iterator1,iterator2);
又稱發佈-訂閱模式,定義了對象之間的一對多依賴,這樣一來,當一個對象改變狀態時,它的全部依賴者都會收到通知並自動更新。
幫你的對象知悉現狀,不會錯過該對象感興趣的事情,對象甚至能夠在運行時決定是否須要繼續被通知,就像你關注了京東商城某款產品的降價信息,當該商品降價,你就會經過短信或者郵件得到通知,而不用你天天都登錄去看了,這種狀況下,京東商城就是主題(subject),做爲客戶的你就是觀察者了。
主題是具備狀態的對象,而且能夠控制這些狀態;
觀察者使用這些狀態,雖然這些狀態不屬於它們;
主題和觀察者之間數據的傳輸有推(push)和拉(pull)兩種,推得方式被認爲更加正確;
普遍應用在異步編程中;
兩者之間經過鬆耦合聯繫在一塊兒;
指定好主題(發佈者);
給主題一個緩存列表,用於存放回調函數以便通知觀察者;
發佈消息時,主題遍歷緩存列表,觸發裏面存放的訂閱者回調函數;
訂閱者接受信息,各自處理;
一個獲取房價信息變化的例子
var salesOffice = {}; //定義售樓處 salesOffice.clienList = []; //緩存列表,存放訂閱者的回調函數 // 註冊爲觀察者 salesOffice.listen = function(key,fn){ if (!this.clienList[key]) { this.clienList[key]=[]; // 若是尚未訂閱過此消息,給該類消息訂閱一個緩存列表 } this.clienList[key].push(fn); //訂閱的消息添加進消息緩存列表 }; // 再也不觀察 salesOffice.remove = function(key,fn){ var fns = this.clienList[key]; if (!fns) { return false; // 無人關注此類消息,直接返回; } if (!fn) { fns&&(fns.length = 0 ); // 沒有傳入具體的回調函數,表示須要取消key對應消息的全部訂閱 }else{ for ( var l = fns.length-1; l >=0;l--){ var _fn = fns[l]; if (_fn===fn) { fns.splice(l,1); // 刪除對應訂閱 } } } }; // 通知函數 salesOffice.trigger = function(){ // 發佈消息 var key = Array.prototype.shift.call(arguments), // 取出消息類型 fns = this.clienList[key]; // 取出該消息對應的函數集合 if (!fns || fns.length === 0) { return false; // 若是沒有訂閱,則返回 } for(var i = 0 , fn; i<fns.length ;fn = fns[i++];){ fn.apply(this,arguments); // arguments 是發佈消息時的參數 } }; salesOffice.listen('squareMeter88'),fn1 = function(price){ console.log('價格='+ price + 'call' + '小明'); }; salesOffice.listen('squareMeter110'),fn2 = function(price){ console.log('價格='+ price + 'call' + '小紅'); }; salesOffice.remove('squareMeter88', fn1); //刪除小明的訂閱 salesOffice.trigger('squareMeter110',3000000);
容許對象在內部狀態改變時改變它的行爲,對象好像看起來修改了它的類。
解決某些須要場景的問題。
將狀態封裝爲獨立的類,並將請求委託給當前的狀態對象,當對象的內部狀態改變時,會帶來不一樣的行爲變化;
不一樣的狀態下有不一樣的行爲;
狀態模式的關鍵是把事物的每種狀態封裝爲單獨的類,跟狀態有關的行爲被封裝在這個類的內部。
var light = function(){ this,currState = FSM.off;//設計默認狀態 this.button = null; }; Light.prototype.init = function(){ var button = document.createElement('button'), self = this; button.innerHtml = '已關燈'; this.button = document.body.appendChild(button); this.button.onclick = function(){ self.currState.buttonWasPress.call(self); } }; var FSM = { off:{ buttonWasPress:function(){ console.log('關燈'); this.button.innerHTML = '下一次按我是開燈'; this.currState = FSM.on; } }, on:{ buttonWasPress:function(){ console.log('開燈'); this.button.innerHTML = '下一次點擊是關燈'; this.currState = FSM.off; } } }; var light = new Light(); light.init();
把類和對象組合到更大的結構中
動態的將責任附加到對象上。它比繼承更具備彈性。
缺點:
在設計中加入大量的小類,致使別人不理解設計方式;
類型問題;
增長代碼的複雜度
增長行爲到包裝對象上,在不改變對象自身的基礎上,在程序運行期間給對象動態的添加職責,好比說點了一杯咖啡,添加其它調料的過程,或者相似於在炒菜的過程當中,加油加鹽加料酒的過程。
裝飾者和被裝飾者具備同樣的類型,也就是有共同的超類;
新的行爲由組合對象獲得;
行爲來自裝飾者和基礎組件,或與其它裝飾者之間的組合關係;
一個衝咖啡的例子
// 被裝飾者 var coffee = function(){ make:function(){ console.log('衝咖啡'); } } //裝飾者1 var sugerDecorator = function(){ console.log('加糖'); } // 裝飾者2 var milkDecorator = function(){ console.log('加奶'); } var coffee1 = coffee.make; coffee.make = function(){ coffee1(); sugerDecorator(); } var coffee2 = coffee.make; coffee.make = function(){ coffee2(); milkDecorator(); } coffee.make(); // 衝咖啡加糖加奶
代理模式爲另外一個對象提供一個替身或佔位符以控制對這個對象的訪問
使用代理模式建立對象,讓表明對象控制某對象的訪問,被代理的對象能夠是遠程的對象,建立開銷大的對象或者須要安全控制的對象。
保護代理用於過濾掉一些請求;
虛擬代理把一些開銷大的請求延遲到真正須要它的時候纔去建立(最經常使用);
類圖
一個圖片預加載的例子
var myImage = (function(){ var imgNode = document.createElement('img'); document.body.appendChild(imgNode); return{ setSrc:function(src){ imgNode.src = src; } } })(); var proxyImage = (function(){ var img = new Image; img.onload = function(){ myImage.setSrc(this.src) } return{ setSrc:function(src){ myImage.setSrc('../loading.gif'); img.src = src; } } })(); proxyImage.setSrc('http;//.../123.jpg');
提供了一個統一的接口
經過實現一個提供更合理的接口的外觀類,能夠將一個複雜的子系統變得容易使用,不只簡化了接口,也將客戶從組件中解耦。
又名包裝器,適配器模式將一個類的接口,轉換爲客戶指望的另外一個接口,適配器讓本來接口不兼容的類能夠合做無間。
類圖
包裝某些對象,讓它們的接口看起來不像本身而像是被的東西,將類的接口轉爲想要的接口,以便實現不一樣的接口;就像你買了港版手機,附帶的港版的充電器,你須要一個轉接頭才能使用,這個轉接頭的功能就相似於適配器。
值得注意的是這是一種亡羊補牢的措施。
客戶經過目標接口調用適配器的方法對適配器發出請求;
適配器使用被適配者接口把請求轉換爲被被適配者的一個或多個接口;
客戶接受到調用的結果,可是並未察覺這一切是適配器在起做用。
對象適配器類圖
類適配器類圖
一個適配器實例
// 適配器模式 var googleMap = { show:function(){ console.log('開始渲染谷歌地圖') } }; var baiduMap = { display:function(){ console.log('開始渲染百度地圖') } }; var baidumapAdapter = { show : function(){ return baiduMap.display(); } }; renderMap(googleMap); renderMap(baiduMapAdapter);
本文由zhangwang首發於簡書和segmentfault,轉載請加以說明。