[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`被裝飾之后,返回的實際上是一個新的函數,如果在原函數上保存了一些屬性,那么這些屬性會丟失。
這種裝飾方式也疊加了函數的作用域,如果裝飾的鏈條過長,性能上也會受到一些影響。