设计模式——装饰器模式

装饰器(decorator)模式可以在不改变对象自身的基础上,在程序运行期间给对像动态的添加方法或属性。与继承相比,装饰者是一种更轻便灵活的作法。缓存

简单说:容许向一个现有的对象添加新的功能,同时又不改变其原有结构。bash

简单的装饰器模式

咱们在修改老项目,或者与同事合做开发一个任务的时候,会发如今一份你没法更改的js文件里定义了window.onload方法,而你也须要在window.onload方法里作些操做,那么这种状况下改怎么办呢?app

// 同事的代码
window.onload = () => {
    console.log('同事的处理逻辑')
}

// 装饰者
let _onload= window.onload || function () {} // 将原函数缓存下来
window.onload = () => {
    _onload()
    console.log('本身的处理函数')
};
复制代码

可是上述写法也有几个弊端:函数

1)须要多维护了一个中间变量_onload,当装饰链比较长或者须要装饰的函数变多的时候,中间变量会愈来愈多。优化

2)会有this被劫持的风险。ui

针对上述两个弊端,接下来就介绍一个AOP函数来优化这种装饰:this

AOP装饰函数

AOP装饰函数是把一些跟核心业务逻辑无关的功能抽离出来,好比说日志统计等;也能够执行一些业务逻辑的前置操做,好比说参数校验等。spa

例如咱们平时写业务逻辑的时候,一般会在最后加上埋点逻辑,以下:prototype

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('业务逻辑');
        }
    },
};
复制代码

装饰器模式和代理模式的区别

装饰器模式和代理模式都是对现有对象功能的扩展,可是二者的出发点不一样。装饰者模式偏向于增长对象功能的同时作到解耦;代理模式偏向于对对象的辅助或者流程的把控。

总结

装饰器模式的优势:

一、能够动态扩展一个对象;

二、复用性较强。能够给一个对象屡次增长同一个装饰器,也能够用同一个装饰器来装饰不一样的对象。

缺点:

一、多层装饰使用起来相对比较复杂。

相关文章
相关标签/搜索