模式是对某情景下,针对某种问题的某种解决方案。而一个设计模式是用来解决一个常常出现的设计问题的经验方法。这么说来,每一个模式均可能有着本身的意图,应用场景,使用方法和使用后果。本文的行文思路和目的皆在于了解各个模式的定义,应用场景和用实例说明如何在前端开发中使用。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,转载请加以说明。