本文已同步到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入门--阮一峰