設計模式——裝飾器模式

裝飾器(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('業務邏輯');
        }
    },
};
複製代碼

裝飾器模式和代理模式的區別

裝飾器模式和代理模式都是對現有對象功能的擴展,可是二者的出發點不一樣。裝飾者模式偏向於增長對象功能的同時作到解耦;代理模式偏向於對對象的輔助或者流程的把控。

總結

裝飾器模式的優勢:

一、能夠動態擴展一個對象;

二、複用性較強。能夠給一個對象屢次增長同一個裝飾器,也能夠用同一個裝飾器來裝飾不一樣的對象。

缺點:

一、多層裝飾使用起來相對比較複雜。

相關文章
相關標籤/搜索