[TOC]
# 装饰者模式
给对象动态地增加职责的方式称为装饰者模式。
装饰者模式能够在不改变对象自身的基础上,在程序运行期间给对象动态地添加职责。跟继承相比,装饰者是一种更轻便灵活的做法,这是一种“即用即付”的方式。
## 模拟传统面向对象语言的装饰者模式
```javascript
var Plan = function(){};
Plan.prototype.fire = function(){
console.log('普通子弹');
};
var MissileDecorator = function(plan){
this.plan = plan;
};
MissileDecorator.prototype.fire = function(){
this.plan.fire();
console.log('导弹');
};
var AtomDecorator = function(plan){
this.plan = plan;
};
AtomDecorator.prototype.fire = function(){
this.plan.fire();
console.log('原子弹');
};
var plan = new Plan();
plan = new MissileDecorator(plan);
plan = new AtomDecorator(plan);
plan.fire();
```
这种给对象动态增加职责的方式,并没有真正地改动对象自身,而是将对象放入另一个对象之中,这些对象以一条链的方式进行引用,形成一个聚合对象。这些对象都拥有相同的接口,当请求到达链中的某个对象时,这个对象会执行自身的操作,随后把请求转发给链中的下一个对象。
## 装饰者也是包装器
装饰者模式将一个对象嵌入另一个对象之中,实际上相当于这个对象被另一个对象包装起来,形成一条包装链。
## 回到JavaScript的装饰者
```javascript
var plan = {
fire: function(){
console.log('普通子弹');
}
};
var missilDecorator = function(){
console.log('导弹');
};
var atomDecorator = function(){
console.log('原子弹');
};
var fire1 = plane.fire;
plan.fire = function(){
fire1();
missilDecorator();
};
var fire2 = plan.fire;
plan.fire = function(){
fire2();
atomDecorator();
};
plan.fire();
```
## 装饰函数
通过保存原引用的方式就可以改写某个函数
```javascript
var a = function(){
alert(1);
};
var _a = a;
a = function(){
_a();
alert(2);
};
a();
```
但是这种方式存在两个问题:
- 必须维护一个中间变量,如果装饰函数的装饰链较长,或者需要装饰的函数变多,这些中间变量的数量也会越来越多。
- 还会遇到`this`被劫持的问题。
## 用AOP装饰函数
给出`Function.prototype.before`和`Function.prototype.after`方法
```javascript
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`指向原函数,然后返回一个“代理”函数,这个“代理”函数只是结构上像代理而已,并不承担代理的职责。它的工作是把请求分别转发给新添加的函数和原函数,且负责保证它们的执行顺序,让新添加的函数在原函数之前执行,这样就实现了动态装饰的效果。
## AOP的应用实例
用`AOP`装饰函数的技巧在实际开发中非常有用。不论是业务代码的编写,还是在框架层面,都可以把行为依照职责分成粒度更细的函数,随后通过装饰把它们合并到一起,有助于编写一个松耦合和高复用性的系统。
### 数据统计上报
```javascript
Function.prototype.after = function(afterFn){
var _self = this;
return function(){
var ret = _self.apply(this, arguments);
afterFn.apply(this, arguments);
return ret;
}
};
var showLogiun = function(){
console.log('打开登录浮层');
};
var log = function(){
console.log('上报数据');
};
showLogin = showLogin.after(log);
document.getElementById('login').onclick = showLogin;
```
### 用AOP动态改变函数的参数
```javascript
Function.prototype.before = function(beforeFn){
var _self = this;
return function(){
beforefn.apply(this, arguments);
return _self.apply(this, arguments);
}
};
var func = function(param){
console.log(param); // 输出:{a: 'a', b:'b'}
};
func = func.before(function(param){
param.b = 'b';
});
func({a: 'a'});
```
### 插件式的表单验证
```javascript
Function.prototype.before = function(beforeFn){
var _self = this;
return function(){
if(beforefn.apply(this, arguments) === false){
return;
};
return _self.apply(this, arguments);
}
};
var validata = function(){
if(username.value === ''){
alert('不能为空');
return false;
}
if(password.value === ''){
alert('不能为空');
return false;
}
};
var formSubmit = function(){
var param = {
username: username.value,
password: password.value
};
ajax('....');
};
formSubmit = formSubimt.before(validata);
submitBtn.onclick = function(){
formSubmit();
};
```
函数通过`Function.prototype.before`或者`Function.prototype.after`被装饰之后,返回的实际上是一个新的函数,如果在原函数上保存了一些属性,那么这些属性会丢失。
这种装饰方式也叠加了函数的作用域,如果装饰的链条过长,性能上也会受到一些影响。