前端开发中常见的设计模式代码实例讲解
设计模式的定义是,在面向对象软件设计过程中针对特定问题的简洁而优雅的解决方案。在不同的编程语言中,对设计模式的实现其实是可能会有区别的。比如java和javascript,在Java这种静态编译型语言中,无法动态地给已存在的对象添加职责,所以一般通过包装类的方式来实现装饰者模式。但在javascript这种动态解释型语言中,给对象动态添加职责是再简单不过的事情。这就造成了javascript语言的装饰者模式不再关注于给对象动态添加职责,而是关注于给函数动态添加职责。本篇博文将介绍以下几个比较常见的设计模式:
单例模式 观察者模式 命令模式 职责链模式
单例模式
单例模式的定义是保证一个类只有一个实例,并且提供一个访问它的全局访问点。有些时候一些对象我们往往只需要一个,比如线程池、全局缓存、浏览器中的window对象等。单例模式的优点是:
可以用来划分命名空间,减少全局变量的数量 使用单例模式可以使代码组织的更为一致,使代码容易阅读和维护 可以被实例化,且实例化一次
要实现一个标准的单例模式并不复杂,无非是用一个变量标识当前是否已经为某个类创建过对象,如果是,则在下一次获取这个类的实例时,直接返回之前创建的对象。下面是单例模式的基本结构:
// 单例模式 var Singleton = function(name){ this.name = name; this.instance = null; }; Singleton.prototype.getName = function(){ return this.name; }; // 获取实例对象 Singleton.getInstance = function(name) { if(!this.instance) { this.instance = new Singleton(name); } return this.instance; }; // 测试单例模式的实例 var a = Singleton.getInstance("aa"); var b = Singleton.getInstance("bb");
实际上因为单例模式是只实例化一次,所以a和b其实是相等的。也即是说下面语句的值为true。
console.log(a===b)
由于单例模式只实例化一次,因此第一次调用,返回的是a实例的对象,继续调用的时候,b的实例也就是a的实例,因此下面打印的都是aa:
console.log(a.getName());// aa console.log(b.getName());// aa
观察者模式
观察者模式又叫做发布-订阅模式,它定义了对象间的一种一对多的依赖关系,当一个对象的状态发生变化时,所有依赖于他的对象都将得到通知,在javascript的开发中,一般用事件模型来替代传统的发布 — 订阅模式。
发布 — 订阅模式可以广泛应用于异步编程中,这是一种替代传递回调函数的方案。比如,我们可以订阅 ajax请求的 error 、 succ 等事件。或者如果想在动画的每一帧完成之后做一些事情,那我们可以订阅一个事件,然后在动画的每一帧完成之后发布这个事件。在异步编程中使用发布 — 订阅模式,我们就无需过多关注对象在异步运行期间的内部状态,而只需要订阅感兴趣的事件发生点。
发布 — 订阅模式还可以取代对象之间硬编码的通知机制,一个对象不用再显式地调用另外一个对象的某个接口。发布 — 订阅模式让两个对象松耦合地联系在一起,虽然不太清楚彼此的细节,但这不影响它们之间相互通信。当有新的订阅者出现时,发布者的代码不需要任何修改;同样发布者需要改变时,也不会影响到之前的订阅者。只要之前约定的事件名没有变化,就可以自由地改变它们。
实际上,只要我们曾经在 DOM 节点上面绑定过事件函数,那我们就曾经使用过发布 — 订阅模式,以下代码便是一个示例:
document.body.addEventListener( 'click', function(){ alert(2);}, false ); document.body.click(); // 模拟用户点击
在这里需要监控用户点击 document.body 的动作,但是我们没办法预知用户将在什么时候点击。所以我们订阅 document.body 上的 click 事件,当 body 节点被点击时, body 节点便会向订阅者发布这个消息。就像是楼房购买,购房者不知道房子什么时候开售,于是他在订阅消息后等待售楼处发布消息。
除了 DOM 事件,我们还会经常实现一些自定义的事件,这种依靠自定义事件完成的发布 —订阅模式可以用于任何 javascript代码中。实现发布 — 订阅模式的步骤如下:
1. 首先指定好谁充当发布者;
2. 然后给发布者添加一个缓存列表,用于存放回调函数以便通知订阅者;
3. 最后发布消息的时候,发布者会遍历这个缓存列表,依次触发里面存放的订阅者回调函数。
var salesOffices = {}; // 定义售楼处 salesOffices.clientList = []; // 缓存列表,存放订阅者的回调函数 salesOffices.listen = function( fn ){ // 增加订阅者 this.clientList.push( fn ); // 订阅的消息添加进缓存列表 }; salesOffices.trigger = function(){ // 发布消息 for( var i = 0, fn; fn = this.clientList[ i++ ]; ){ fn.apply( this, arguments ); // arguments 是发布消息时带上的参数 } }; //调用 salesOffices.listen( function( price, squareMeter ){//订阅消息 console.log( '价格= ' + price ); console.log( 'squareMeter= ' + squareMeter ); }); salesOffices.trigger( 2000000, 88 ); // 输出:200 万,88 平方米
至此,实现了最简单的发布-订阅模式。比起在Java中实现观察者模式,还是有不同的,在Java里面实现,通常会把订阅者对象当成引用传入发布者对象中,同时订阅者对象还需提供一个名为诸如 update的方法,供发布者对象在适合的时候调用。而在 javascript中,我们用注册回调函数的形式来代替传统的发布 — 订阅模式。
命令模式
命令模式中的命令(command)指的是一个执行某些特定事情的指令。
命令模式的应用场景是:有时候需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是什么,此时希望用一种松耦合的方式来设计软件,使得请求发送者和请求接收者能够消除彼此之间的耦合关系。
传统的面向对象的模式设计代码的方式是,假设html结构如下:
<button id="button1">刷新菜单目录</button> <button id="button2">增加子菜单</button> <button id="button3">删除子菜单</button>
javascript的代码如下:
var b1 = document.getElementById("button1"), b2 = document.getElementById("button2"), b3 = document.getElementById("button3"); // 定义setCommand 函数,该函数负责往按钮上面安装命令。点击按钮后会执行command对象的execute()方法。 var setCommand = function(button,command){ button.onclick = function(){ command.execute(); } }; // 下面我们自己来定义各个对象来完成自己的业务操作 var MenuBar = { refersh: function(){ alert("刷新菜单目录"); } }; var SubMenu = { add: function(){ alert("增加子菜单"); }, del: function(){ alert("删除子菜单"); } }; // 下面是编写命令类 var RefreshMenuBarCommand = function(receiver){ this.receiver = receiver; }; RefreshMenuBarCommand.prototype.execute = function(){ this.receiver.refersh(); } // 增加命令操作 var AddSubMenuCommand = function(receiver) { this.receiver = receiver; }; AddSubMenuCommand.prototype.execute = function() { this.receiver.add(); } // 删除命令操作 var DelSubMenuCommand = function(receiver) { this.receiver = receiver; }; DelSubMenuCommand.prototype.execute = function(){ this.receiver.del(); } // 最后把命令接收者传入到command对象中,并且把command对象安装到button上面 var refershBtn = new RefreshMenuBarCommand(MenuBar); var addBtn = new AddSubMenuCommand(SubMenu); var delBtn = new DelSubMenuCommand(SubMenu); setCommand(b1,refershBtn); setCommand(b2,addBtn); setCommand(b3,delBtn);
不过上述代码太过繁琐,用javascript的回调函数,接收者被封闭在回调函数产生的环境中,执行操作将会更加简单,仅仅执行回调函数即可。
var setCommand = function(button,func) { button.onclick = function(){ func(); } }; var MenuBar = { refersh: function(){ alert("刷新菜单界面"); } }; var SubMenu = { add: function(){ alert("增加菜单"); } }; // 刷新菜单 var RefreshMenuBarCommand = function(receiver) { return function(){ receiver.refersh(); }; }; // 增加菜单 var AddSubMenuCommand = function(receiver) { return function(){ receiver.add(); }; }; var refershMenuBarCommand = RefreshMenuBarCommand(MenuBar); // 增加菜单 var addSubMenuCommand = AddSubMenuCommand(SubMenu); setCommand(b1,refershMenuBarCommand); setCommand(b2,addSubMenuCommand);
职责链模式
职责链模式的定义是:使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系,将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。
假设有这样的场景,我们负责一个售卖手机的电商网站,经过分别交纳 500元定金和 200元定金的两轮预定后(订单已在此时生成),现在已经到了正式购买的阶段。公司针对支付过定金的用户有一定的优惠政策。在正式购买后,已经支付过 500元定金的用户会收到 100元的商城优惠券,200元定金的用户可以收到 50元的优惠券,而之前没有支付定金的用户只能进入普通购买模式,也就是没有优惠券,且在库存有限的情况下不一定保证能买到。
orderType :表示订单类型(定金用户或者普通购买用户), code 的值为 1的时候是 500元定金用户,为 2的时候是 200元定金用户,为 3的时候是普通购买用户。 pay :表示用户是否已经支付定金,值为 true 或者 false , 虽然用户已经下过 500元定金的订单,但如果他一直没有支付定金,现在只能降级进入普通购买模式 stock :表示当前用于普通购买的手机库存数量,已经支付过 500 元或者 200 元定金的用户不受此限制
把这个流程代码化:
var order = function( orderType, pay, stock ){ if ( orderType === 1 ){ // 500 元定金购买模式 if ( pay === true ){ // 已支付定金 console.log( '500 元定金预购, 得到 100 优惠券' ); }else{ // 未支付定金,降级到普通购买模式 if ( stock > 0 ){ // 用于普通购买的手机还有库存 console.log( '普通购买, 无优惠券' ); }else{ console.log( '手机库存不足' ); } } } else if ( orderType === 2 ){ // 200 元定金购买模式 if ( pay === true ){ console.log( '200 元定金预购, 得到 50 优惠券' ); }else{ if ( stock > 0 ){ console.log( '普通购买, 无优惠券' ); }else{ console.log( '手机库存不足' ); } } } else if ( orderType === 3 ){ if ( stock > 0 ){ console.log( '普通购买, 无优惠券' ); }else{ console.log( '手机库存不足' ); } } }; order( 1 , true, 500); // 输出: 500 元定金预购, 得到 100 优惠券
现在我们采用职责链模式重构这段代码,先把 500 元订单、200 元订单以及普通购买分成 3个函数。
接下来把 orderType 、 pay 、 stock 这 3个字段当作参数传递给 500元订单函数,如果该函数不符合处理条件,则把这个请求传递给后面的 200元订单函数,如果 200元订单函数依然不能处理该请求,则继续传递请求给普通购买函数,代码如下:
// 500 元订单 var order500 = function( orderType, pay, stock ){ if ( orderType === 1 && pay === true ){ console.log( '500 元定金预购, 得到 100 优惠券' ); }else{ order200( orderType, pay, stock ); // 将请求传递给 200 元订单 } }; // 200 元订单 var order200 = function( orderType, pay, stock ){ if ( orderType === 2 && pay === true ){ console.log( '200 元定金预购, 得到 50 优惠券' ); }else{ orderNormal( orderType, pay, stock ); // 将请求传递给普通订单 } }; // 普通购买订单 var orderNormal = function( orderType, pay, stock ){ if ( stock > 0 ){ console.log( '普通购买, 无优惠券' ); }else{ console.log( '手机库存不足' ); } }; // 测试结果: order500( 1 , true, 500); // 输出:500 元定金预购, 得到 100 优惠券 order500( 1, false, 500 ); // 输出:普通购买, 无优惠券 order500( 2, true, 500 ); // 输出:200 元定金预购, 得到 500 优惠券 order500( 3, false, 500 ); // 输出:普通购买, 无优惠券 order500( 3, false, 0 ); // 输出:手机库存不足
可以看到,执行结果和前面那个巨大的 order 函数完全一样,但是代码的结构已经清晰了很多,我们把一个大函数拆分了 3个小函数,去掉了许多嵌套的条件分支语句。