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();
我们看到MissileDecorator和AtomDecoratord的构造函数接受plane参数,并且保存,它们的fire方法中除了输出还调用了plane对象的fire方法。这种给对象动态添加职责的方法,并没有真正的改动对象自身,而是把一个对象放到另一个对象中,这些对象以一条链的方式调用,形成一个聚合对象,并且这些对象都有同样的接口,对用户来说是透明的,用户不需要关系这个对象是否被装饰过,我们可以递归的嵌套多个装饰者对象。
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();
装饰函数
我们在JavaScript中为函数添加功能最简单的办法就是直接改写函数,但是这也是最粗暴的办法,违反了开放-封闭原则
例如:
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();
这样我们并没有去修改原来的代码,但是也存在一些问题,比如必须维护_a这个变量,this被劫持了,虽然上面的代码体现不出来,我们看看这个代码:
var _getElementById = document.getElementById;
document.getElementById = function(id) {
alert(1);
return _getElementById(id)
}
var button = document.getElementById('button');
这段代码执行后会报错,因为_getElementById是一个全局函数,调用后this指向的是window,但是document.getElemntById
内部的实现里面this的实现是指向document的,所以这种方式有问题
解决方法其实很简单,利用apply就可以啦
var _getElementById = document.getElementById;
document.getElementById = function(){
alert(1);
return _getElementById.apply(document, arguments);
}
var button = docuemnt.getElementById('button');
但是这种方式很麻烦,我们看如何使用AOP来实现
用AOP装饰函数
我们之前第一章讲过AOP的高阶函数应用:
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;
};
}
我们在看如何利用before来解决上一节的问题
document.getElementById = document.getElementById.before(function(){
alert(1);
});
var button = document.getElementById('button');
不过这种方式是在Function.prototype上添加的方法,这种其实是污染原型的,我们可以稍微修改一下:
var before = function(fn, beforefn) {
return function() {
beforefn.apply(this, arguments);
return fn.apply(this, arguments);
}
}
var a = before(function(){
alert(1);
}, function(){
alert(2);
});
a = before(a, function(){
alert(3);
});
a();
装饰者模式和代理模式
我们回想一下发现装饰者模式和代理模式很像,两者都是描述了如何给对象提供一定程度上的引用,他们实现上也都保留了对另外一个对象的引用,并且向那个对象发送请求。
装饰者模式和代理模式最大的区别在于他们的意图和目的。代理模式的目的是当本体不方便访问的时候,给本体提供一个替代者,它强调的是一种关系,并且这种关系y开始就可以被确定了;而装饰者模式用于一开始不能确定对象的全部功能,代理模式通常只有一层代理-本体的引用,而装饰者模式通常会形成一条长长的装饰链。