对象动态地增加职责的方式称为装饰者(decorator)模式。装饰者模式能够在不改变对象自身的基础上,在程序运行期间给对象动态地添加职责。跟继承相比,装饰者是一种更轻便灵活的做法,这是一种“即用即付”的方式。
1. 模拟传统面向对象语言的装饰者模式
- 假设我们在编写一个飞机大战的游戏,随着经验值的增加,我们操作的飞机对象可以升级成更厉害的飞机,一开始这些飞机只能发射普通的子弹,升到第二级时可以发射导弹,升到第三级时可以发射原子弹。
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(); // 分别输出: 发射普通子弹、发射导弹、发射原子弹
这种给对象动态增加职责的方式,并没有真正地改动对象自身,而是将对象放入另一个对象之中,这些对象以一条链的方式进行引用,形成一个聚合对象。这些对象都拥有相同的接口(fire方法),当请求达到链中的某个对象时,这个对象会执行自身的操作,随后把请求转发给链中的下一个对象。
因为装饰者对象和它所装饰的对象拥有一致的接口,所以它们对使用该对象的客户来说是透明的,被装饰的对象也并不需要了解它曾经被装饰过,这种透明性使得我们可以递归地嵌套任意多个装饰者对象。
2. 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();
// 分别输出: 发射普通子弹、发射导弹、发射原子弹
3. 装饰函数
//要想为函数添加一些功能,最简单粗暴的方式就是直接改写该函数,但这是最差的办法,直接违反了开放封闭原则:
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 代码,但是这种方式存在以下两个问题:
-
必须维护
_a这个中间变量,虽然看起来并不起眼,但如果函数的装饰链较长,或者需要装饰的函数变多,这些中间变量的数量也会越来越多。
-
this 被劫持的问题
4. 用AOP装饰函数
首先给出 Function.prototype.before 方法和 Function.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
}
}
- 使用
<html>
<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>
</html>
- AOP 实现是在 Function.prototype 上添加 before 和 after 方法,但许多人不喜欢这种污染原型的方式,那么我们可以做一些变通,把原函数和新函数都作为参数传入before 或者 after 方法:
var before = function (fn, beforefn) {
return function () {
beforefn.apply(this, arguments)
return fn.apply(this, arguments)
}
}
var a = before(
function () {
alert(3)
},
function () {
alert(2)
}
)
a = before(a, function () {
alert(1)
})
a()
5. 装饰者模式和代理模式
装饰者模式和第代理模式的结构看起来非常相像,这两种模式都描述了怎样为对象提供一定程度上的间接引用,它们的实现部分都保留了对另外一个对象的引用,并且向那个对象发送请求。
代理模式和装饰者模式最重要的区别在于它们的意图和设计目的。代理模式的目的是,当直接访问本体不方便或者不符合需要时,为这个本体提供一个替代者。本体定义了关键功能,而代理提供或拒绝对它的访问,或者在访问本体之前做一些额外的事情。装饰者模式的作用就是为对象动态加入行为。换句话说,代理模式强调一种关系(Proxy 与它的实体之间的关系),这种关系可以静态的表达,也就是说,这种关系在一开始就可以被确定。而装饰者模式用于一开始不能确定对象的全部功能时。代理模式通常只有一层代理本体的引用,而装饰者模式经常会形成一条长长的装饰链。