本文已同步到Github JavaScript中常見的設計模式,若是感受寫的還能夠,就給個小星星吧,歡迎star和收藏。html
最近拜讀了曾探大神的《JavaScript設計模式與開發實踐》,真是醍醐灌頂,猶如打通任督二脈的感受,讓我對JavaScript的理解加深了不少。git
本文中關於各類設計模式定義都是引用書中的,部分引用自百度百科已標出。另外,本文中所舉例子大可能是書中的,自已作了一些修改和補充,用ES6(書中都是ES5的方式)的方式實現,以加深本身對「類」的理解,並非本身來說解設計模式,主要是作一些筆記以方便本身事後複習與加深理解,同時也但願把書中典型的例子整理出來和你們分享,共同探討和進步。es6
一提起設計模式,相信你們都會脫口而出,23種設計模式,五大設計原則。這裏就不說了,奈何我功力遠遠不夠啊。下面把我整理出的經常使用JavaScript設計模式按類型作個表格整理。本文較長,若是閱讀起來不方便,可連接到個人github中,單獨查看每一種設計模式。先整理這些,後續會繼續補充,感興趣的同窗能夠關注。github
模式分類 | 名稱 |
---|---|
建立型 | 工廠模式 |
單例模式 | |
原型模式 | |
結構型 | 適配器模式 |
代理模式 | |
行爲型 | 策略模式 |
迭代器模式 | |
觀察者模式(發佈-訂閱模式) | |
命令模式 | |
狀態模式 |
工廠模式中,咱們在建立對象時不會對客戶端暴露建立邏輯,而且是經過使用一個共同的接口來指向新建立的對象,用工廠方法代替new操做的一種模式。
class Creator { create(name) { return new Animal(name) } } class Animal { constructor(name) { this.name = name } } var creator = new Creator() var duck = creator.create('Duck') console.log(duck.name) // Duck var chicken = creator.create('Chicken') console.log(chicken.name) // Chicken
小結:算法
舉一個書中登陸框的例子,代碼以下:segmentfault
<!DOCTYPE html> <html lang="en"> <body> <button id="btn">登陸</button> </body> <script> class Login { createLayout() { var oDiv = document.createElement('div') oDiv.innerHTML = '我是登陸框' document.body.appendChild(oDiv) oDiv.style.display = 'none' return oDiv } } class Single { getSingle(fn) { var result; return function() { return result || (result = fn.apply(this, arguments)) } } } var oBtn = document.getElementById('btn') var single = new Single() var login = new Login() // 因爲閉包,createLoginLayer對result的引用,因此當single.getSingle函數執行完以後,內存中並不會銷燬result。 // 當第二次之後點擊按鈕,根據createLoginLayer函數的做用域鏈中已經包含了result,因此直接返回result // 講獲取單例和建立登陸框的方法解耦,符合開放封閉原則 var createLoginLayer = single.getSingle(login.createLayout) oBtn.onclick = function() { var layout = createLoginLayer() layout.style.display = 'block' } </script> </html>
小結:設計模式
1.單例模式的主要思想就是,實例若是已經建立,則直接返回數組
function creatSingleton() { var obj = null // 實例如已經建立過,直接返回 if (!obj) { obj = xxx } return obj }
2.符合開放封閉原則瀏覽器
用原型實例指定建立對象的種類,而且經過拷貝這些原型建立新的對象。-- 百度百科
在JavaScript中,實現原型模式是在ECMAScript5中,提出的Object.create方法,使用現有的對象來提供新建立的對象的__proto__。緩存
var prototype = { name: 'Jack', getName: function() { return this.name } } var obj = Object.create(prototype, { job: { value: 'IT' } }) console.log(obj.getName()) // Jack console.log(obj.job) // IT console.log(obj.__proto__ === prototype) //true
更多關於prototype的知識能夠看我以前的JavaScript中的面向對象、原型、原型鏈、繼承,下面列一下關於prototype的一些使用方法
1. 方法繼承
var Parent = function() {} Parent.prototype.show = function() {} var Child = function() {} // Child繼承Parent的全部原型方法 Child.prototype = new Parent()
2. 全部函數默認繼承Object
var Foo = function() {} console.log(Foo.prototype.__proto__ === Object.prototype) // true
3. Object.create
var proto = {a: 1} var propertiesObject = { b: { value: 2 } } var obj = Object.create(proto, propertiesObject) console.log(obj.__proto__ === proto) // true
4. isPrototypeOf
prototypeObj是否在obj的原型鏈上
prototypeObj.isPrototypeOf(obj)
5. instanceof
contructor.prototype是否出如今obj的原型鏈上
obj instanceof contructor
6. getPrototypeOf
Object.getPrototypeOf(obj) 方法返回指定對象obj的原型(內部[[Prototype]]屬性的值)
Object.getPrototypeOf(obj)
7. setPrototypeOf
設置一個指定的對象的原型 ( 即, 內部[[Prototype]]屬性)到另外一個對象或 null
var obj = {} var prototypeObj = {} Object.setPrototypeOf(obj, prototypeObj) console.log(obj.__proto__ === prototypeObj) // true
舉一個書中渲染地圖的例子
class GooleMap { show() { console.log('渲染谷歌地圖') } } class BaiduMap { show() { console.log('渲染百度地圖') } } function render(map) { if (map.show instanceof Function) { map.show() } } render(new GooleMap()) // 渲染谷歌地圖 render(new BaiduMap()) // 渲染百度地圖
可是假如BaiduMap類的原型方法不叫show,而是叫display,這時候就可使用適配器模式了,由於咱們不能輕易的改變第三方的內容。在BaiduMap的基礎上封裝一層,對外暴露show方法。
class GooleMap { show() { console.log('渲染谷歌地圖') } } class BaiduMap { display() { console.log('渲染百度地圖') } } // 定義適配器類, 對BaiduMap類進行封裝 class BaiduMapAdapter { show() { var baiduMap = new BaiduMap() return baiduMap.display() } } function render(map) { if (map.show instanceof Function) { map.show() } } render(new GooleMap()) // 渲染谷歌地圖 render(new BaiduMapAdapter()) // 渲染百度地圖
小結:
本文舉一個使用代理對象加載圖片的例子來理解代理模式,當網絡很差的時候,圖片的加載須要一段時間,這就會產生空白,影響用戶體驗,這時候咱們可在圖片真正加載完以前,使用一張loading佔位圖片,等圖片真正加載完再給圖片設置src屬性。
class MyImage { constructor() { this.img = new Image() document.body.appendChild(this.img) } setSrc(src) { this.img.src = src } } class ProxyImage { constructor() { this.proxyImage = new Image() } setSrc(src) { let myImageObj = new MyImage() myImageObj.img.src = 'file://xxx.png' //爲本地圖片url this.proxyImage.src = src this.proxyImage.onload = function() { myImageObj.img.src = src } } } var proxyImage = new ProxyImage() proxyImage.setSrc('http://xxx.png') //服務器資源url
本例中,本體類中有本身的setSrc方法,若是有一天網絡速度已經不須要預加載了,咱們能夠直接使用本體對象的setSrc方法,,而且不須要改動本體類的代碼,並且能夠刪除代理類。
// 依舊能夠知足需求 var myImage = new MyImage() myImage.setSrc('http://qiniu.sunzhaoye.com/CORS.png')
小結:
定義一系列的算法,把它們一個個封裝起來,並使它們能夠替換
var fnA = function(val) { return val * 1 } var fnB = function(val) { return val * 2 } var fnC = function (val) { return val * 3 } var calculate = function(fn, val) { return fn(val) } console.log(calculate(fnA, 100))// 100 console.log(calculate(fnB, 100))// 200 console.log(calculate(fnC, 100))// 300
直接上代碼, 實現一個簡單的迭代器
class Creater { constructor(list) { this.list = list } // 建立一個迭代器,也叫遍歷器 createIterator() { return new Iterator(this) } } class Iterator { constructor(creater) { this.list = creater.list this.index = 0 } // 判斷是否遍歷完數據 isDone() { if (this.index >= this.list.length) { return true } return false } next() { return this.list[this.index++] } } var arr = [1, 2, 3, 4] var creater = new Creater(arr) var iterator = creater.createIterator() console.log(iterator.list) // [1, 2, 3, 4] while (!iterator.isDone()) { console.log(iterator.next()) // 1 // 2 // 3 // 4 }
ES6中的迭代器:
JavaScript中的有序數據集合包括:
注意: Object不是有序數據集合
以上有序數據集合都部署了Symbol.iterator屬性,屬性值爲一個函數,執行這個函數,返回一個迭代器,迭代器部署了next方法,調用迭代器的next方法能夠按順序訪問子元素
以數組爲例測試一下,在瀏覽器控制檯中打印測試以下:
var arr = [1, 2, 3, 4] var iterator = arr[Symbol.iterator]() console.log(iterator.next()) // {value: 1, done: false} console.log(iterator.next()) // {value: 2, done: false} console.log(iterator.next()) // {value: 3, done: false} console.log(iterator.next()) // {value: 4, done: false} console.log(iterator.next()) // {value: undefined, done: true}
小結:
先實現一個簡單的發佈-訂閱模式,代碼以下:
class Event { constructor() { this.eventTypeObj = {} } on(eventType, fn) { if (!this.eventTypeObj[eventType]) { // 按照不一樣的訂閱事件類型,存儲不一樣的訂閱回調 this.eventTypeObj[eventType] = [] } this.eventTypeObj[eventType].push(fn) } emit() { // 能夠理解爲arguments借用shift方法 var eventType = Array.prototype.shift.call(arguments) var eventList = this.eventTypeObj[eventType] for (var i = 0; i < eventList.length; i++) { eventList[i].apply(eventList[i], arguments) } } remove(eventType, fn) { // 若是使用remove方法,fn爲函數名稱,不能是匿名函數 var eventTypeList = this.eventTypeObj[eventType] if (!eventTypeList) { // 若是沒有被人訂閱改事件,直接返回 return false } if (!fn) { // 若是沒有傳入取消訂閱的回調函數,則改訂閱類型的事件所有取消 eventTypeList && (eventTypeList.length = 0) } else { for (var i = 0; i < eventTypeList.length; i++) { if (eventTypeList[i] === fn) { eventTypeList.splice(i, 1) // 刪除以後,i--保證下輪循環不會漏掉沒有被遍歷到的函數名 i--; } } } } } var handleFn = function(data) { console.log(data) } var event = new Event() event.on('click', handleFn) event.emit('click', '1') // 1 event.remove('click', handleFn) event.emit('click', '2') // 不打印
以上代碼能夠知足先訂閱後發佈,可是若是先發布消息,後訂閱就不知足了。這時候咱們能夠稍微修改一下便可知足先發布後訂閱,在發佈消息時,把事件緩存起來,等有訂閱者時再執行。代碼以下:
class Event { constructor() { this.eventTypeObj = {} this.cacheObj = {} } on(eventType, fn) { if (!this.eventTypeObj[eventType]) { // 按照不一樣的訂閱事件類型,存儲不一樣的訂閱回調 this.eventTypeObj[eventType] = [] } this.eventTypeObj[eventType].push(fn) // 若是是先發布,則在訂閱者訂閱後,則根據發佈後緩存的事件類型和參數,執行訂閱者的回調 if (this.cacheObj[eventType]) { var cacheList = this.cacheObj[eventType] for (var i = 0; i < cacheList.length; i++) { cacheList[i]() } } } emit() { // 能夠理解爲arguments借用shift方法 var eventType = Array.prototype.shift.call(arguments) var args = arguments var that = this function cache() { if (that.eventTypeObj[eventType]) { var eventList = that.eventTypeObj[eventType] for (var i = 0; i < eventList.length; i++) { eventList[i].apply(eventList[i], args) } } } if (!this.cacheObj[eventType]) { this.cacheObj[eventType] = [] } // 若是先訂閱,則直接訂閱後發佈 cache(args) // 若是先發布後訂閱,則把發佈的事件類型與參數保存起來,等到有訂閱後執行訂閱 this.cacheObj[eventType].push(cache) } }
小結:
--百度百科
在命令的發佈者和接收者之間,定義一個命令對象,命令對象暴露出一個統一的接口給命令的發佈者,而命令的發佈者不用去管接收者是如何執行命令的,作到命令發佈者和接收者的解耦。
舉一個若是頁面中有3個按鈕,給不一樣按鈕添加不一樣功能的例子,代碼以下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>cmd-demo</title> </head> <body> <div> <button id="btn1">按鈕1</button> <button id="btn2">按鈕2</button> <button id="btn3">按鈕3</button> </div> <script> var btn1 = document.getElementById('btn1') var btn2 = document.getElementById('btn2') var btn3 = document.getElementById('btn3') // 定義一個命令發佈者(執行者)的類 class Executor { setCommand(btn, command) { btn.onclick = function() { command.execute() } } } // 定義一個命令接收者 class Menu { refresh() { console.log('刷新菜單') } addSubMenu() { console.log('增長子菜單') } } // 定義一個刷新菜單的命令對象的類 class RefreshMenu { constructor(receiver) { // 命令對象與接收者關聯 this.receiver = receiver } // 暴露出統一的接口給命令發佈者Executor execute() { this.receiver.refresh() } } // 定義一個增長子菜單的命令對象的類 class AddSubMenu { constructor(receiver) { // 命令對象與接收者關聯 this.receiver = receiver } // 暴露出統一的接口給命令發佈者Executor execute() { this.receiver.addSubMenu() } } var menu = new Menu() var executor = new Executor() var refreshMenu = new RefreshMenu(menu) // 給按鈕1添加刷新功能 executor.setCommand(btn1, refreshMenu) var addSubMenu = new AddSubMenu(menu) // 給按鈕2添加增長子菜單功能 executor.setCommand(btn2, addSubMenu) // 若是想給按鈕3增長刪除菜單的功能,就繼續增長刪除菜單的命令對象和接收者的具體刪除方法,而沒必要修改命令對象 </script> </body> </html>
舉一個關於開關控制電燈的例子,電燈只有一個開關,第一次按下打開弱光,第二次按下打開強光,第三次按下關閉。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>state-demo</title> </head> <body> <button id="btn">開關</button> <script> // 定義一個關閉狀態的類 class OffLightState { constructor(light) { this.light = light } // 每一個類都須要這個方法,在不一樣狀態下按都須要觸發這個方法 pressBtn() { this.light.setState(this.light.weekLightState) console.log('開啓弱光') } } // 定義一個弱光狀態的類 class WeekLightState { constructor(light) { this.light = light } pressBtn() { this.light.setState(this.light.strongLightState) console.log('開啓強光') } } // 定義一個強光狀態的類 class StrongLightState { constructor(light) { this.light = light } pressBtn() { this.light.setState(this.light.offLightState) console.log('關閉電燈') } } class Light { constructor() { this.offLightState = new OffLightState(this) this.weekLightState = new WeekLightState(this) this.strongLightState = new StrongLightState(this) this.currentState = null } setState(newState) { this.currentState = newState } init() { this.currentState = this.offLightState } } let light = new Light() light.init() var btn = document.getElementById('btn') btn.onclick = function() { light.currentState.pressBtn() } </script> </body> </html>
若是這時候須要增長一個超強光,則只需增長一個超強光的類,並添加pressBtn方法,改變強光狀態下,點擊開關須要把狀態更改成超強光,超強光狀態下,點擊開關把狀態改成關閉便可,其餘代碼都不須要改動。
class StrongLightState { constructor(light) { this.light = light } pressBtn() { this.light.setState(this.light.superLightState) console.log('開啓超強光') } } class SuperLightState { constructor(light) { this.light = light } pressBtn() { this.light.setState(this.light.offLightState) console.log('關閉電燈') } } class Light { constructor() { this.offLightState = new OffLightState(this) this.weekLightState = new WeekLightState(this) this.strongLightState = new StrongLightState(this) this.superLightState = new SuperLightState(this) this.currentState = null } setState(newState) { this.currentState = newState } init() { this.currentState = this.offLightState } }
小結:
終於到最後可,歷時多日地閱讀與理解,並記錄與整理筆記,目前整理出10中JavaScript中常見的設計模式,後續會對筆記繼續整理,而後加以補充。因爲筆者功力比較淺,若有問題,還望你們多多指正,謝謝。
參考文章:
JavaScript設計模式與開發實踐
深刻理解JavaScript系列/設計模式--湯姆大叔的博客
設計模式--菜鳥教程
JavaScript 中常見設計模式整理
ES6入門--阮一峯