装饰者模式(使用装饰函数装饰对象)可以动态地给某个对象添加一些额外的职责,而不会影响从这个类中派生的其他对象
在 JavaScript 中,几乎一切都是对象,其中函数又被称为一等对象。在平时的开发工作中,也许大部分时间都在和函数打交道。在 JavaScript 中可以很方便地给某个对象扩展属性和方法,但却很难在不改动某个函数源代码的情况下,给该函数添加一些额外的功能。在代码的运行期间,我们很难切入某个函数的执行环境。
要想为函数添加一些功能,最简单粗暴的方式就是直接改写该函数,但这是最差的办法,直接违反了开放-封闭原则
例子:
天气冷了, 就添加衣服来保暖;天气热了, 就将外套脱下;这个例子很形象地含盖了装饰器的神韵, 随着天气的冷暖变化, 衣服可以动态的穿上脱下。
let wear = function() {
console.log('穿上第一件衣服')
}
let wear = function() {
console.log('穿上第一件衣服')
console.log('穿上第二件衣服')
}
通过保存原引用的方式就可以改写某个函数:
let wear = function() {
console.log('穿上第一件衣服')
}
const _wear1 = wear
wear = function() {
_wear1()
console.log('穿上第二件衣服')
}
const _wear2 = wear
wear = function() {
_wear2()
console.log('穿上第三件衣服')
}
wear()
// 穿上第一件衣服
// 穿上第二件衣服
// 穿上第三件衣服
这种方式有以下缺点:
使用用 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;
}
};
用后置代码来实验下上面穿衣服的例子:
const wear1 = function() {
console.log('穿上第一件衣服')
}
const wear2 = function() {
console.log('穿上第二件衣服')
}
const wear3 = function() {
console.log('穿上第三件衣服')
}
const wear = wear1.after(wear2).after(wear3)
wear()
// 穿上第一件衣服
// 穿上第二件衣服
// 穿上第三件衣服
但这样子有时会污染原生函数, 可以装饰函数抽离出来
const after = function(fn, afterFn) {
return function() {
fn.apply(this, arguments)
afterFn.apply(this, arguments)
}
}
const wear = after(after(wear1, wear2), wear3)
wear()
AOP 的应用实例
用AOP动态改变函数的参数
Function.prototype.before = function(beforefn) {
var __self = this;
return function() {
beforefn.apply(this, arguments); // (1)
return __self.apply(this, arguments); // (2)
}
}
从 这 段 代 码 的 (1)处 和 (2)处 可 以 看 到 , beforefn 和 原 函 数 __self 共 用 一 组 参 数 列 表arguments,当我们在 beforefn 的函数体内改变 arguments 的时候,原函数__self 接收的参数列表自然也会变化。
var func = function(param) {
console.log(param); // 输出: {a: "a", b: "b"}
}
func = func.before(function(param) {
param.b = 'b';
});
func({
a: 'a' });
例子:
现在有一个用于发起 ajax 请求的函数,这个函数负责项目中所有的 ajax 异步请求:
var ajax = function( type, url, param ){
console.dir(param);
// 发送 ajax 请求的代码略
};
ajax( 'get', 'http:// xxx.com/userinfo', {
name: 'sven' } );
如果需要再参数中添加Token 参数(防止 CSRF 攻击),且希望ajax为一个干净的函数,不修改原函数:
var getToken = function() {
return 'Token';
}
ajax = ajax.before(function(type, url, param) {
param.Token = getToken();
});
ajax('get', 'http:// xxx.com/userinfo', {
name: 'sven' });
// ajax 请求的参数 {name: "sven", Token: "Token"}
装饰者模式和代理模式的比较:
装饰者模式和代理模式的结构看起来非常相像,这两种模式都描述了怎样为对象提供一定程度上的间接引用,它们的实现部分都保留了对另外一个对象的引用,并且向那个对象发送请求。
代理模式和装饰者模式最重要的区别在于它们的意图和设计目的。
代理模式的目的是,当直接访问本体不方便或者不符合需要时,为这个本体提供一个替代者。本体定义了关键功能,而代
理提供或拒绝对它的访问,或者在访问本体之前做一些额外的事情。
装饰者模式的作用就是为对象动态加入行为。
换句话说,代理模式强调一种关系( Proxy 与它的实体之间的关系),这种关系可以静态的表达,也就是说,这种关系在一开始就可以被确定。
而装饰者模式用于一开始不能确定对象的全部功能时。
代理模式通常只有一层代理-本体的引用,而装饰者模式经常会形成一条长长的装饰链。
总结
这种模式在实际开发中非常有用,除了上面提到的例子,它在框架开发中也十分有用。作为框架作者,我们希望框架里的函数提供的是一些稳定而方便移植的功能,那些个性化的功能可以在框架之外动态装饰上去,这可以避免为了让框架拥有更多的功能,而去使用一些 if、 else 语句预测用户的实际需要。