装饰者模式
在程序开发中,许多时候都并不希望某个类天生就非常庞大,一次性包含许多职责。那么我们就可以使用装饰者模式。装饰者模式可以动态地给某个对象添加一些额外的职责,而不会影响从这个类中派生的其他对象。
level01:模拟传统面向对象语言的装饰者模式
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<script>
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();
// 分别输出: 发射普通子弹、发射导弹、发射原子弹
</script>
</body>
</html>
导弹类和原子弹类的构造函数都接受参数 plane 对象,并且保存好这个参数,在它们的 fire方法中,除了执行自身的操作之外,还调用 plane 对象的 fire 方法。
这种给对象动态增加职责的方式,并没有真正地改动对象自身,而是将对象放入另一个对象之中,这些对象以一条链的方式进行引用,形成一个聚合对象。这些对象都拥有相同的接口( fire
方法),当请求达到链中的某个对象时,这个对象会执行自身的操作,随后把请求转发给链中的
下一个对象。
因为装饰者对象和它所装饰的对象拥有一致的接口,所以它们对使用该对象的客户来说是透明的,被装饰的对象也并不需要了解它曾经被装饰过,这种透明性使得我们可以递归地嵌套任意多个装饰者对象。
level02:回到 JavaScript 的装饰者
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<script>
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();
}
//前者plane.fire()嵌套在后者plane.fire()里
var fire2 = plane.fire;
plane.fire = function(){
fire2();
atomDecorator();
}
plane.fire();
// 分别输出: 发射普通子弹、发射导弹、发射原子弹
</script>
</body>
</html>
level01:装饰函数
var a = function(){
alert (1);
}
// 改成:
var a = function(){
alert (1);
alert (2);
}
缺点:直接违反了开放封闭原则。
level02:装饰函数
很多时候我们不想去碰原函数,也许原函数是由其他同事编的,里面的实现非常杂乱。甚至在一个古老的项目中,这个函数的源代码被隐藏在一个我们不愿碰触的阴暗角落里。现在需要一个办法,在不改变函数源代码的情况下,能给函数增加功能,这正是开放-封闭原则给我们指出的光明道路。
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);
}
这样的代码当然是符合开放封闭原则的,我们在增加新功能的时候,确实没有修改原来的window.onload 代码,但是这种方式存在以下两个问题。
- 必须维护 _onload 这个中间变量,虽然看起来并不起眼,但如果函数的装饰链较长,或者需要装饰的函数变多,这些中间变量的数量也会越来越多。
- 其实还遇到了 this 被劫持的问题,在 window.onload 的例子中没有这个烦恼,是因为调用
普通函数 _onload 时, this 也指向 window ,跟调用 window.onload 时一样(函数作为对象的方法被调用时, this 指向该对象,所以此处 this 也只指向 window )。
在把 window.onload换成 document.getElementById ,代码如下:
var _getElementById = document.getElementById;
document.getElementById = function( id ){
alert (1);
return _getElementById( id ); // (1)
}
var button = document.getElementById( 'button' );
执行这段代码,我们看到在弹出 alert(1) 之后,紧接着控制台抛出了异常:// 输出: Uncaught TypeError: Illegal invocation
异常发生在(1) 处的 _getElementById( id ) 这句代码上,此时 _getElementById 是一个全局函数,当调用一个全局函数时, this 是指向 window 的,而 document.getElementById 方法的内部实现需要使用 this 引用, this 在这个方法内预期是指向 document ,而不是 window , 这是错误发生的原因,所以使用现在的方式给函数增加功能并不保险。
改进后的代码可以满足需求,我们要手动把 document 当作上下文 this 传入 _getElementById :
<html>
<button id="button"></button>
<script>
var _getElementById = document.getElementById;
document.getElementById = function(){
alert (1);
return _getElementById.apply( document, arguments );
}
var button = document.getElementById( 'button' );
</script>
</html>
装饰者模式和代理模式
装饰者模式和代理模式的结构看起来非常相像,这两种模式都描述了怎样为对象提供一定程度上的间接引用,它们的实现部分都保留了对另外一个对象的引用,并且向那个对象发送请求。
代理模式和装饰者模式最重要的区别在于它们的意图和设计目的。代理模式的目的是,当接访问本体不方便或者不符合需要时,为这个本体提供一个替代者。本体定义了关键功能,而代理提供或拒绝对它的访问,或者在访问本体之前做一些额外的事情。装饰者模式的作用就是为对象动态加入行为。换句话说,代理模式强调一种关系(Proxy与它的实体之间的关系),这种关系可以静态的表达,也就是说,这种关系在一开始就可以被确定。而装饰者模式用于一开始不能确定对象的全部功能时。代理模式通常只有一层代理本体的引用,而装饰者模式经常会形成一条长长的装饰链。
在虚拟代理实现图片预加载的例子中,本体负责设置 img 节点的 src,代理则提供了预加载的功能,这看起来也是“加入行为”的一种方式,但这种加入行为的方式和装饰者模式的偏重点
是不一样的。装饰者模式是实实在在的为对象增加新的职责和行为,而代理做的事情还是跟本体一样,最终都是设置 src。但代理可以加入一些“聪明”的功能,比如在图片真正加载好之前,
先使用一张占位的 loading图片反馈给客户。