装饰器(decorator)模式能够在不改变对象自身的基础上,在程序运行期间给对像动态的添加方法或属性。与继承相比,装饰者是一种更轻便灵活的做法。
简单说:允许向一个现有的对象添加新的功能,同时又不改变其原有结构。
简单的装饰器模式
我们在修改老项目,或者与同事合作开发一个任务的时候,会发现在一份你无法更改的js文件里定义了window.onload方法,而你也需要在window.onload方法里做些操作,那么这种情况下改怎么办呢?
// 同事的代码
window.onload = () => {
console.log('同事的处理逻辑')
}
// 装饰者
let _onload= window.onload || function () {} // 将原函数缓存下来
window.onload = () => {
_onload()
console.log('自己的处理函数')
};
但是上述写法也有几个弊端:
1)需要多维护了一个中间变量_onload,当装饰链比较长或者需要装饰的函数变多的时候,中间变量会越来越多。
2)会有this被劫持的风险。
针对上述两个弊端,接下来就介绍一个AOP函数来优化这种装饰:
AOP装饰函数
AOP装饰函数是把一些跟核心业务逻辑无关的功能抽离出来,比如说日志统计等;也可以执行一些业务逻辑的前置操作,比如说参数校验等。
例如我们平时写业务逻辑的时候,通常会在最后加上埋点逻辑,如下:
function event () {
console.log('业务逻辑');
console.log('埋点逻辑');
}
event();
这种写法在功能上没有什么问题,但是其实我们可以把业务逻辑和埋点逻辑分离开,让代码逻辑显得更清晰。
Function.prototype.after = function(afterfn) {
let _this = this; // 将原函数的引用缓存下来
return function () { // 返回一个添加了自己逻辑(afterfn)的新函数
let ret = _this.apply(this, arguments) // 执行原函数,并且保存住返回结果
afterfn.apply(this, arguments) // 执行自己的函数,并且保证this不会被劫持,同时自己的函数接受的参数和原函数一样
return ret
}
}
function event () {
console.log('业务逻辑');
}
// 利用after装饰器给event加上埋点逻辑
event = event.after(() => {
console.log('埋点逻辑');
})
event();
再次优化onload方法
利用上面的AOP装饰函数,我们回过头来看下最初对window.onload的装饰,针对前面提出的两点弊端,还可以再优化一下:
Function.prototype.after = function(afterfn) {
let _this = this
return function () {
let ret = _this.apply(this, arguments)
afterfn.apply(this, arguments)
return ret
}
}
window.onload = function(){
console.log('同事的处理逻辑')
}
window.onload = ( window.onload || function(){} ).after(function(){
console.log('自己的处理函数')
}.after(function(){
console.log('自己的第二个处理函数')
});
而且这种写法是可以在后面不停加 .after(function () {}) ,可以连续的加上各个逻辑块。
说到装饰器模式,其实很容易联想到ES7的装饰器,那么我们来简单看看ES7的装饰器。
ES7的装饰器
我们自己先建一个class,作为基础类。
class Base {
constructor(val = 1){
this.init(val);
}
init (val) {
this.val = val;
}
getVal() {
console.log(this.val)
}
}
var base = new Base();
console.log(base.getVal()); // 1
现在我们要做在Base初始化数据时默认加上100作为基数,可以给init方法加个装饰器,写法如下:
function decorateInit(target, key, descriptor) {
const method = descriptor.value; //method就是init的原函数
let baseNum = 100;
descriptor.value = (...args)=>{ // 重写原函数
args[0] += baseNum;
let ret = method.apply(target, args); // 将修改后的参数传递给原函数
return ret;
}
return descriptor;
}
class Base {
constructor(val = 1){
this.init(val);
}
@decorateInit
init (val) {
this.val = val;
}
getVal() {
console.log(this.val)
}
}
var base = new Base();
console.log(base.getVal()); // 101
首先看到在init方法上多了个 @decorateInit ,这个其实就是装饰器。
而在 decorateInit 方法中接受3个参数,这里插一个小知识点。在ES6的class语法糖中“”
class A {
sayHello () {
console.log('hello world');
}
}
在给类添加方法时,其实是调用 Object.defineProperty 这个方法,而 Object.defineProperty 方法我们都知道接受三个参数,target 、name 和 descriptor。所以创建一个Class并且添加一个方法等价于下面这段代码。
function A () {}
Object.defineProperty(A.prototype, 'sayHello', {
value: function() { console.log('hello world'); },
enumerable: false,
configurable: true,
writable: true
})
而装饰器也是利用了 Object.defineProperty,所以传参也就一致了。第一个参数为类的原型对象,第二个参数为要装饰的属性名,第三个参数是该属性的描述对象。当然,装饰器也是可以传参的,看下面一种写法:
function decorateInit(num)
return function (target, key, descriptor) {
const method = descriptor.value; //method就是init的原函数
let baseNum = num || 100;
descriptor.value = (...args)=>{ // 重写原函数
args[0] += baseNum;
let ret = method.apply(target, args); // 将修改后的参数传递给原函数
return ret;
}
return descriptor;
}
}
class Base {
constructor(val = 1){
this.init(val);
}
@decorateInit(20)
init (val) {
this.val = val;
}
getVal() {
console.log(this.val)
}
}
var base = new Base();
console.log(base.getVal()); // 21
其实就是在原有的装饰器函数外再嵌套一层函数。
Vue中的装饰器
利用前面的知识,我们先写个AOP装饰函数(after)绑定在Function原型上,然后在装饰器(log)中重写函数(装饰器模式),以达到先执行业务逻辑,再执行装饰器中的埋点逻辑的目的。最后在Vue的methods里给具体的方法加上装饰器。具体代码如下:
// 第一部分就是AOP装饰函数
Function.prototype.after = function (afterfn) {
let _self = this;
return function () {
let ret = _self.apply(this, arguments);
afterfn.apply(this, arguments);
return ret;
};
};
// 第二部分是一个能接收参数的log装饰器函数
const log = function (stat) {
return function (target, key, descriptor) {
descriptor.value = descriptor.value.after(() => {
console.log('埋点逻辑' + stat)
});
};
};
// 以上两个方法其实可以封装在基础类中
// Vue项目组件中可以添加装饰器
export default {
name: 'List',
data () {
return {}
},
methods: {
@log('123456')
event () {
console.log('业务逻辑');
}
},
};
装饰器模式和代理模式的区别
装饰器模式和代理模式都是对现有对象功能的扩展,但是两者的出发点不同。装饰者模式偏向于增加对象功能的同时做到解耦;代理模式偏向于对对象的辅助或者流程的把控。
总结
装饰器模式的优点:
1、可以动态扩展一个对象;
2、复用性较强。可以给一个对象多次增加同一个装饰器,也可以用同一个装饰器来装饰不同的对象。
缺点:
1、多层装饰使用起来相对比较复杂。