装饰器模式,又名装饰者模式。它的定义是“在不改变原对象的基础上,通过对其进行包装拓展,使原有对象可以满足用户的更复杂需求”。
装饰函数
在JavaScript中,几乎一切都是对象,其中函数又被称为一等对象。在平时的开发工作中,也许大部分时间都在和函数打交道。在JavaScript中可以很方便地给某个对象扩展属性和方法,但却很难在不改动某个函数源代码的情况下,给该函数添加一些额外的功能。在代码的运行期间,我们很难切入某个函数的执行环境。
要想为函数添加一些功能,最简单粗暴的方式就是直接改写该函数,但这是最差的办法,直接违反了开放-封闭原则:
const func = function() {
console.log('我是Func的原有逻辑');
}
// 改成
const func = function() {
console.log('我是Func的原有逻辑');
console.log('我是Func的装饰器逻辑');
}
很多时候我们不想去碰原函数,也许原函数是由其他同事编写的,里面的实现非常杂乱。甚至在一个古老的项目中,这个函数的源代码被隐藏在一个我们不愿碰触的阴暗角落里。现在需要一个办法,在不改变函数源代码的情况下,能给函数增加功能,这正是开放-封闭原则给我们指出的光明道路。
用AOP装饰函数
首先给出Function.prototype.before方法和Function.prototype.after方法:
Function.prototype.before = function( beforefn ){
let __self = this; // 保存原函数的引用
return function(){ // 返回包含了原函数和新函数的"代理"函数
beforefn.apply( this, arguments ); // 执行新函数,且保证this不被劫持,新函数接受的参数
// 也会被原封不动地传入原函数,新函数在原函数之前执行
return __self.apply( this, arguments ); // 执行原函数并返回原函数的执行结果,
// 并且保证this不被劫持
}
}
Function.prototype.after = function( afterfn ){
let __self = this;
return function(){
let ret = __self.apply( this, arguments );
afterfn.apply( this, arguments );
return ret;
}
};
Function.prototype.before接受一个函数当作参数,这个函数即为新添加的函数,它装载了新添加的功能代码。
接下来把当前的this保存起来,这个this指向原函数,然后返回一个“代理”函数,这个“代理”函数只是结构上像代理而已,并不承担代理的职责(比如控制对象的访问等)。它的工作是把请求分别转发给新添加的函数和原函数,且负责保证它们的执行顺序,让新添加的函数在原函数之前执行(前置装饰),这样就实现了动态装饰的效果。
我们注意到,通过Function.prototype.apply来动态传入正确的this,保证了函数在被装饰之后,this不会被劫持。
用AOP动态改变函数的参数
现在有一个用于发起request请求的函数,这个函数负责项目中所有的request异步请求:
const request = function(type, url, params) {
// 发送请求代码...
}
request('get', 'http://xxx.com/userinfo', {name: 'gosnails'});
request函数在项目中一直运转良好。直到有一天,我们的网站遭受了CSRF攻击。解决CSRF攻击最简单的一个办法就是在HTTP请求中带上一个Token参数。
假设我们已经有一个用于生成Token的函数:
const getToken = function() {
return 'Token';
}
现在的任务是给每个request请求都加上Token参数:
const request = function( type, url, param ){
param = param || {};
Param.Token = getToken();
// 发送请求代码...
};
虽然已经解决了问题,但我们的request函数相对变得僵硬了,每个从request函数里发出的请求都自动带上了Token参数,虽然在现在的项目中没有什么问题,但如果将来把这个函数移植到其他项目上,或者把它放到一个开源库中供其他人使用,Token参数都将是多余的。
为了解决这个问题,把Token参数通过Function.prototyte.before装饰到request函数的参数param对象中:
request = request.before(function( type, url, param ){
param.Token = getToken();
});
用AOP的方式给request函数动态装饰上Token参数,保证了request函数是一个相对纯净的函数,提高了request函数的可复用性,它在被迁往其他项目的时候,不需要做任何修改。
装饰类
在项目开发的结尾阶段难免要加上很多统计数据的代码,假如页面中有一个Button,点击这个Button除了业务代码,与此同时还要进行数据上报,来统计有多少用户点击了这个Button。
传统面向对象装饰器
class Button {
onClick() {
console.log('我是业务逻辑');
}
}
class classDecorator {
constructor(button) {
this.button = button;
}
onClick() {
this.button.onClick();
console.log('我是数据上报逻辑');
}
}
// 验证装饰器是否生效
let button = new Button();
button = new classDecorator(button);
button.onClick();
装饰类的构造函数接受参数button对象,并且保存好这个参数,在它的onClick方法中,除了执行自身的数据上报逻辑之外,还调用button对象的onClick方法。
这种给对象动态增加职责的方式,并没有真正地改动对象自身,而是将对象放入另一个对象之中,这些对象以一条链的方式进行引用,形成一个聚合对象。
ES7 中的装饰器
在 ES7 中,我们可以通过一个@语法糖轻松地给一个类装上装饰器:
// 装饰器函数,它的第一个参数是目标类
function classDecorator(target) {
target.hasDecorator = true;
return target;
}
// 将装饰器“安装”到Button类上
@classDecorator
class Button {
// Button类的相关逻辑
}
// 验证装饰器是否生效
console.log('Button 是否被装饰了:', Button.hasDecorator);
也可以用同样的语法糖去装饰类里面的方法:
function funcDecorator(target, name, descriptor) {
let originalMethod = descriptor.value;
descriptor.value = function() {
let ret = originalMethod.apply(this, arguments);
console.log('我是数据统计逻辑');
return ret;
}
return descriptor;
}
class Button {
@funcDecorator
onClick() {
console.log('我是业务逻辑');
}
}
// 验证装饰器是否生效
const button = new Button();
button.onClick();
总结
通过动态改变函数参数、数据上报这两个例子,我们了解了装饰函数和装饰类,这种模式在实际开发中非常有用,装饰器模式可以避免为了让框架拥有更多的功能,而去使用一些if、else语句预测用户的实际需要。