《JavaScript设计模式与开发实践》——第十五章(装饰者模式)学习记录

给对象动态地增加职责的方式称为装饰者模式。装饰者模式能够在不改变对象自身的基础上,在程序运行期间给对象动态地添加职责。跟继承相比,装饰者是一种更轻便灵活的做法,这是一种“即用即付”的方式。

模拟传统面向对象语言的装饰者模式

假设我们在编写一个飞机大战的游戏,随着经验值的增加,飞机对象可以升级成更厉害的飞机,一开始这些飞机只能发射普通的子弹,升到第二级时可以发射导弹,升到第三级时可以发射原子弹。

// 首先是原始的飞机类
var Plane = function(){}
Plane.prototype.fire = function(){
  console.log("发射普通子弹");
}
// 增加两个装饰类,分别是导弹和原子弹
var MissileDecorator = function(plane){
  this.plane = plane;
}
MissileDecorator.prototype.fire = function(){
  this.plane.fire();
  console.log("发射导弹");

}
var AtomDecorator = function(plane){
  this.plane = plane;
}
AtomDecorator.prototype.fire = function(){
  this.plane.fire();
  console.log("发射原子弹");
}
// 测试一下:
var plane = new Plane();
plane = new MissileDecorator(plane);
plane = new AtomDecorator(plane);
plane.fire();

导弹类和原子弹类的构造函数都接受参数plane对象,并且保存好这个参数,在它们的fire方法中,除了执行自身的操作之外,还调用plane对象的fire方法。
这种给对象动态增加职责的方式,并没有真正地改动对象自身,而是将对象放入另一个对象之中,这些对象以一条链的方式进行引用,形成一个聚合对象。这些对象都拥有相同的接口(fire方法),当请求达到链中的某个对象时,这个对象会执行自身的操作,随后把请求转发给链中的下一个对象。

JavaScript的装饰者

我们可以直接改写对象或者对象的某个方法,并不需要使用“类”来实现装饰者模式:

var plane = {
  fire:function(){
    console.log("发射普通子弹");
  }
}
var missileDecorator = function(){
  console.log("发射导弹");
}
var atomDecorator = function(){
  console.log("发射原子弹");
}

var fire1 = plane.fire;
plane.fire = function(){
  fire1();
  missileDecorator();
}

var fire2 = plane.fire;
plane.fire = function(){
  fire2();
  atomDecorator();
}
plane.fire();

要想为函数添加一些功能,最简单粗暴的方式就是直接改写该函数,但这是最差的办法,直接违反了开放-封闭原则。
比如:

//将
var a = function(){
  alert(1);
}
//改成:
var a = function(){
  alert(1);
  alert(2);
}

很多时候我们不想去修改原函数,所以可以通过保存原引用的方式改写某个函数:

var a = function(){
  alert(1);
}
var _a = a;
a = function(){
  _a();
  alert(2)
}
a();

这是实际开发中很常见的一种做法,比如我们想给window绑定onload事件,但是又步确定这个事件是不是已经被其他人绑定过,为了避免覆盖掉之前的window.onload函数中的行为,我们一般都会先保存好原先的

window.onload,把它放入新的window.onload里执行:
window.onload = function(){
  alert(1)
}
var _onload = window.onload || function(){};
window.onload = function(){
  _onload();
  alert(2)
}

上面的代码符合开闭原则,但是存在两个问题:

  1. 必须维护_onload这个中间变量,如果函数的装饰链较长,或者需要装饰的函数变多,这些中间变量的数量会越来越多。
  2. 还会遇到this被劫持的问题,如:
var _getElementById = document.getElementById;
document.getElementById = function(id){
  alert(1);
  return _getElementById(id);
}
var button = document.getElementById('button');

可以看到抛出异常:
《JavaScript设计模式与开发实践》——第十五章(装饰者模式)学习记录_第1张图片
此时_getElementById是一个全局函数,当调用一个全局函数时,this是指向window的,而document.getElementById方法的内部实现需要使用this引用,this在这个方法内预期是指向document,而不是window,这是错误发生的原因。
改进一下代码,我们要手动把document当作上下文this传入_getElementById:

var _getElementById = document.getElementById;
document.getElementById = function(){
  alert(1);
  return _getElementById.apply(document,arguments);
}
var button = document.getElementById('button');

这种形式还不太方便,接下来看一下AOP装饰函数。

使用AOP装饰函数

首先了解Function.prototype.beforeFunction.prototype.after方法:

Function.prototype.before = function(beforefn){
  var _self = this;//保存原函数的引用
  return function(){//返回包含了原函数和新函数的“代理”函数
    //执行新函数,且保证this不被劫持,新函数接受的参数
    // 也会被原封不动地传入原函数,新函数在原函数之前执行
    beforefn.apply(this,arguments);
    //执行原函数并返回原函数的执行结果,并且保证this不被劫持
    return _self.apply(this,arguments);
  }
}
Function.prototype.after = function(afterfn){
  var _self = this;
  return function(){
    var ret = _self.apply(this,arguments);
    afterfn.apply(this,arguments);
    return ret;
  }
}

Function.prototype.before接受一个函数当作参数,这个函数即为新添加的函数,它装载了新添加的功能代码。
接下来把当前的this保存起来,这个this指向原函数,然后返回一个“代理”函数,这个“代理”函数只是结构上像代理,并不承担代理的职责,它的工作是把请求分别转发给新添加的函数和原函数,且负责保证它们的执行顺序,让新添加的函数在原函数之前执行(前置装饰),这样就实现了动态装饰的效果。
接下来试一下:

<button id="button"></button>
document.getElementById = document.getElementById.before(function(){
  alert(111);
});
var button = document.getElementById('button');
console.log(button)

在window.onload的例子基础上使用after函数:

window.onload = function(){
  alert(1)
};
window.onload = (window.onload || function(){}).after(function(){
  alert(2);
}).after(function(){
  alert(3)
}).after(function(){
  alert(4)
})

有些人不喜欢这种污染原型的方式,我们可以把原函数和新函数都作为参数传入before或者after的方法:

var before = function(fn,beforefn){
  return function(){
    beforefn.apply(this,arguments);
    return fn.apply(this,arguments)
  }
}
var a = before(
  function(){alert(2)},
  function(){alert(3)}
);
a = before(a,function(){alert(1);});
a();

AOP的应用实例

1.数据统计上报
分离业务代码和数据统计代码,无论在什么语言中,都是AOP的经典应用之一。
比如页面中有一个登录button,点击这个button会弹出登录浮层,与此同时要进行数据上报,来统计有多少用户点击了这个登录button:

<button tag="login" id="button">点击打开登录浮层</button>
var showLogin = function(){
  console.log('打开登录浮层');
  log(this.getAttribute('tag'));
}
var log = function(tag){
  console.log('上报标签为:'+tag);
}
document.getElementById('button').onclick = showLogin;

打开登录浮层和数据上报被耦合在一个函数里,使用AOP分离之后:

Function.prototype.after = function(afterfn){
  var _self = this;
  return function(){
    var ret = _self.apply(this,arguments);
    afterfn.apply(this,arguments);
    return ret;
  }
}
var showLogin = function(){
  console.log('打开登录浮层');
}
var log = function(){
  console.log('上报标签为:'+this.getAttribute('tag'));
}
showLogin = showLogin.after(log);//打开登录浮层之后上报数据
document.getElementById('button').onclick = showLogin;

2.用AOP动态改变函数的参数

Function.prototype.before = function(beforefn){
  var _self = this;//保存原函数的引用
  return function(){//返回包含了原函数和新函数的“代理”函数
    //执行新函数,且保证this不被劫持,新函数接受的参数
    // 也会被原封不动地传入原函数,新函数在原函数之前执行
    beforefn.apply(this,arguments);
    //执行原函数并返回原函数的执行结果,并且保证this不被劫持
    return _self.apply(this,arguments);
  }
}
var func = function(param){
  console.log(param);
  
}
func = func.before(function(param){
  console.log("111",this)
    param.b = 'b'
  })
func({'a':'a'})//{a: 'a', b: 'b'}

现在有一个用于发起ajax请求的函数,这个函数负责项目中所有的ajax异步请求:

var ajax = function(type,url,param){
  console.dir(param);
  // 发送ajax请求的代码
}
ajax('get','http://xxx.com/userinfo',{name:'sven'});

现在需要在HTTP请求中带一个Token参数:

var getToken = function(){
  return 'Token';
}
// 现在的任务是给每一个ajax请求都加上Token参数:
var ajax = function(type,url,param){
  param = param || {};
  param.Token = getToken();
  console.dir(param);
}
ajax('get','http://xxx.com/userinfo',{name:'sven'});

虽然解决了问题,但我们的ajax函数相对变得僵硬了,如果另一个项目不需要验证Token,或者是Token的生成方式不同,都必须重新修改ajax函数。
为了解决这个问题,先把ajax函数还原成一个干净的函数:

var ajax = function(type,url,param){
  console.dir(param);//{Token: "Token",name: "sven"}
  // 发送ajax请求的代码
}
// 把Token参数通过before装饰到ajax函数的参数param对象中:
var getToken = function(){
  return 'Token';
}
// 现在的任务是给每一个ajax请求都加上Token参数:
ajax = ajax.before(function(type,url,param){
  param.Token = getToken();
})
ajax('get','http://xxx.com/userinfo',{name:'sven'});

明显可以看到,用AOP的方式给ajax函数动态装饰上Token参数,保证了ajax函数是一个相对纯净的函数,提高了ajax函数的可复用性,它在被迁往其他项目的时候,不需要做任何修改。
3.插件式的表单验证
表单验证对我们来说并不陌生,我们经常要在表单数据提交之前做一下校验,比如登录的时候需要验证用户名和密码是否为空:

var username = document.getElementById('username'),
    password = document.getElementById('password'),
    submitBtn = document.getElementById('submitBtn');

var formSubmit = function(){
  if(username.value === ''){
    return alert('用户名不能为空');
  }
  if(password.value === ''){
    return alert('密码不能为空');
  }
  var param = {
    username:username.value,
    password:password.value
  }
  // 提交代码略
  console.log("代码提交");
}
submitBtn.onclick = function(){
  formSubmit();
}

此时的formSubmit有两个职责,提交和验证,这样的代码比较臃肿,职责混乱。
接下来分离校验输入和提交:

var validata = function(){
  if(username.value === ''){
    alert('用户名不能为空');
    return false;
  }
  if(password.value === ''){
    alert('密码不能为空');
    return false;
  }
}
var formSubmit = function(){
  if(validata() === false){//校验未通过
    return;
  }
  var param = {
    username:username.value,
    password:password.value
  }
   // 提交代码略
   console.log("代码提交");
}
submitBtn.onclick = function(){
  formSubmit();
}

接下来进一步优化这段代码,使validata和formSubmit完全分离开来。先改写Function.prototype.before,如果beforefn的执行结果返回false,表示不再执行后面的原函数:

Function.prototype.before = function(beforefn){
  var _self = this;
  return function(){
    if(beforefn.apply(this,arguments) === false){
      //beforefn返回false的情况直接return
      return;
    };
    return _self.apply(this,arguments);
  }
}
var validata = function(){
  if(username.value === ''){
    alert('用户名不能为空');
    return false;
  }
  if(password.value === ''){
    alert('密码不能为空');
    return false;
  }
}
var formSubmit = function(){
  var param = {
    username:username.value,
    password:password.value
  }
   // 提交代码略
   console.log("代码提交");
}
formSubmit = formSubmit.before(validata);
submitBtn.onclick = function(){
  formSubmit();
}

这段代码中,校验输入和提交表单的代码完全分离开来。

你可能感兴趣的:(javascript设计模式,javascript,设计模式,学习)