JavaScript装饰者模式

1 什么是装饰者模式

在程序开发中,许多时候都我们并不希望某个类天生就非常庞大,一次性包含许多职责。那么我们就可以使用装饰者模式。

装饰者模式可以动态地给某个对象添加一些额外的职责,而不会影响从这个类中派生的其他对象,它能够在不改变对象自身的基础上,在程序运行期间给对象动态地添加职责。

2 模拟装饰者模式

假设我们在编写一个飞机大战的游戏,随着经验值的增加,我们操作的飞机对象可以升级成更厉害的飞机,一开始这些飞机只能发射普通的子弹,升到第二级时可以发射导弹,升到第三级时可以发射原子弹,接下来用代码实现这个过程,首先定义一个飞机类:

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("发射原子弹");
};

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

接下来测试一下:

var plane = new Plane();
plane = new MissileDecorator(plane);
plane = new AtomDecorator(plane);
plane.fire();

// 发射普通子弹
// 发射导弹
// 发射原子弹

3 JavaScript的装饰者

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();
// 分别输出: 发射普通子弹、发射导弹、发射原子弹

4 装饰函数

JavaScript中可以很方便地给某个对象扩展属性和方法,但却很难在不改动某个函数源代码的情况下,给该函数添加一些额外的功能。在代码的运行期间,我们很难切入某个函数的执行环境。要想为函数添加一些功能,最简单粗暴的方式就是直接改写该函数,但这是最差的办法,直接违反了开放封闭原则。

现在需要一个办法,在不改变函数源代码的情况下,能给函数增加功能,通过保存原引用的方式就可以改写某个函数:

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);
};

这种方式存在以下两个问题:

  • 必须维护_onload这个中间变量,虽然看起来并不起眼,但如果函数的装饰链较长,或者需要装饰的函数变多,这些中间变量的数量也会越来越多
  • this被劫持

接下来通过AOP,来提供一种完美的方法给函数动态增加功能。

5 AOP装饰函数

首先给出Function.prototype.before方法和Function.prototype.after方法:

Function.prototype.before = function (beforefn) {
  var __self = this; // 保存原函数的引用
  return function () {
    // 返回包含了原函数和新函数的"代理"函数
    beforefn.apply(this, arguments); // 执行新函数,且保证 this 不被劫持,新函数接受的参数
    // 也会被原封不动地传入原函数,新函数在原函数之前执行
    return __self.apply(this, arguments); // 执行原函数并返回原函数的执行结果,并且保证 this 不被劫持
  };
};
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指向原函数,保证了this不会被劫持,然后返回一个函数。它的工作是把请求分别转发给新添加的函数和原函数,且负责保证它们的执行顺序,让新添加的函数在原函数之前执行(前置装饰),这样就实现了动态装饰的效果。

Function.prototype.after的原理跟Function.prototype.before一模一样,唯一不同的地方在于让新添加的函数在原函数执行之后再执行。

<button id="button"></button>
<script>
  Function.prototype.before = function (beforefn) {
    var __self = this;
    return function () {
      beforefn.apply(this, arguments);
      return __self.apply(this, arguments);
    }
  }
  document.getElementById = document.getElementById.before(function () {
    alert(1);
  });
  var button = document.getElementById('button');
  console.log(button);
</script>

再回到window.onload的例子,用Function.prototype.before来增加新的window.onload事件简单了很多:

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

6 示例:数据统计上报

比如页面中有一个登录button,点击这个button会弹出登录浮层,与此同时要进行数据上报,来统计有多少用户点击了这个登录button:

<button tag="login" id="button">点击打开登录浮层</button>
<script>
  var showLogin = function () {
    console.log('打开登录浮层');
    log(this.getAttribute('tag'));
  }
  var log = function (tag) {
    console.log('上报标签为: ' + tag);
    // (new Image).src = 'http:// xxx.com/report?tag=' + tag; // 真正的上报代码略
  }
  document.getElementById('button').onclick = showLogin;
</script>

我们看到在showLogin函数里,既要负责打开登录浮层,又要负责数据上报,这是两个层面的功能,在此处却被耦合在一个函数里。使用 AOP 分离之后,代码如下:

<button tag="login" id="button">点击打开登录浮层</button>
<script>
  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; 
</script>
  • 17
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值