JavaScript设计模式之装饰者模式

167 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第15天,点击查看活动详情

装饰者模式

装饰者模式(Decorator):在不改变原对象的基础上,通过对其进行包装拓展(添加属性或者方法)使原有对象可以满足用户的更复杂需求。

装饰者用于通过重载方法的形式添加新功能,该模式可以在被装饰者前面或者后面加上自己的行为以达到特定的目的。

例1: 雷霆战机

一开始战机只能发射普通子弹,吃了一个道具或者升级了,发射散弹,再次升级,又可以发射跟踪导弹。
实现代码如下:

var plane = {
  fire: function () {
    console.log('发射普通子弹');
  }
}
plane.fire();     // 发射普通子弹
 
 
var fire1 = plane.fire;
var shot = function () {
  console.log('发射散弹');
}
plane.fire = function () {
  fire1();
  shot();
}
plane.fire();    // 发射普通子弹  发射散弹
 
var fire2 = plane.fire;
var track = function(){
  console.log('发射跟踪导弹');
}
plane.fire = function(){
  fire2();
  track();
}
plane.fire();    // 发射普通子弹  发射散弹  发射跟踪导弹

这样给对象动态的增加职责的方式就没有改变对象自身。
一个对象放入另一个对象,形成了一条装饰链(一个聚合对象)

上面的shot和track也就是装饰者、装饰函数,当函数执行时,会把请求转给链中的下一个对象:

例2

// 需要装饰的类(函数)
function MacBook() {
  this.cost = function () { return 997; };
  this.screenSize = function () { return 11.6; };
}
 
// Decorator 1
function memory( macbook ) {
  var v = macbook.cost();
  macbook.cost = function() {
    return v + 75;
  };
}
 
// Decorator 2
function engraving( macbook ){
  var v = macbook.cost();
  macbook.cost = function(){
    return v + 200;
  };
}
 
// Decorator 3
function insurance( macbook ){
  var v = macbook.cost();
  macbook.cost = function(){
     return v + 250;
  };
}
 
var mb = new MacBook();
memory( mb );
engraving( mb );
insurance( mb );
 
console.log( mb.cost() );          // 1522
console.log( mb.screenSize() );    // 11.6

函数功能拓展

在js中,很容易给对象扩展属性与方法,但是却不容易给函数扩展额外功能,除非改函数源码,但是改写函数违反了开放-封闭原则。

开放封闭原则的主要思想是: 当系统需求发生改变时,尽量不修改系统原有代码功能,应该扩展模块的功能,来实现新的需求。

var foo = function () {
  console.log(1);
}
 
// 修改
var foo = function () {
  console.log(1);
  console.log(2);    // 增
}

一个常用的方法就是缓存函数引用,改写函数

var foo = function () {
  console.log(1);
}
 
var _foo = foo;
foo = function () {
  _foo();
  console.log(2);    
}

但这样写还有有问题:

  • 要维护额外的中间变量(_foo),如果装饰链过长,中间变量就会越来越多
  • 可能会存在this被劫持问题

关于this劫持问题,看下面的例子:

var getEleById = document.getElementById;
document.getElementById = function (id) {
  console.log(1);
  return getEleById(id);
}
document.getElementById('demo');

这样浏览器就会报错:

因为使用document.getElementById的时候,内部有this引用,而这个this期望指向的是document,但是getEleById 在获取了document.getElementById引用后,this就指向了window,导致错误。
为了让this正确指向document,我们可以这样修改:

var getEleById = document.getElementById;
document.getElementById = function (id) {
  console.log(1);
  return getEleById.call(document, id);
}
document.getElementById('demo');

但是这样还是很麻烦,下面我们通过AOP来为装饰者模式实现一个完美的解决方案。

AOP(Aspect Oriented Programming)面向切面编程: 把一些与核心业务逻辑无关的功能抽离出来,再通过“动态织入”方式掺入业务逻辑模块

前置装饰函数

Function.prototype.before = function (beforeFn) {
  var that = this;
  return function () {
    beforeFn.apply(this, arguments);
    return that.apply(this, arguments);
  }
}

在调用beforeFn时,先把原函数的引用保存下来,然后返回一个"代理"函数,这样在原函数调用前,先执行扩展功能的函数,而且他们共用同一个参数列表

后置装饰函数

Function.prototype.after = function (afterFn) {
  var that = this;
  return function () {
    var result = that.apply(this, arguments);
    afterFn.apply(this, arguments);
    return result;
  }
}

后置装饰函数与前置装饰函数基本类型,只是执行顺序不同。

不使用在原型上拓展方法的方式的话, 也可以这么写:

var before = function (originFn, beforeFn) {
  return function () {
    beforeFn.apply(this, arguments);
    return originFn.apply(this, arguments);
  }
}

var after = function (originFn, afterFn) {
  return function () {
    var result = originFn.apply(this, arguments);
    afterFn.apply(this, arguments);
    return result;
  }
}

使用这种方式改造例1:

var fire = function () {
  console.log('发射普通子弹');
}
var shot = function () {
  console.log('发射散弹');
}
var track = function(){
  console.log('发射跟踪导弹');
}

var fn = before(shot, fire);
fn();   // 发射普通子弹  发射散弹

var fn1= after(fn, track);
fn1();  // 发射普通子弹  发射散弹  发射跟踪导弹

缺点

  • 装饰链叠加了函数作用域,如果过长也会产生性能问题
  • 如果原函数上保存了属性,返回新函数后属性会丢失
var demo = function(){
  console.log(1);
}
demo.a = 123;
demo = demo.after(function(){
  console.log(2);
});
demo();
console.log(demo.a);   // undefined

参考资料

JavaScript设计模式与开发实践---曾探

addyosmani.com/resources/e…