向一個現有的對象添加新的功能,同時又不改變其結構的設計模式被稱爲裝飾器模式(Decorator Pattern),它是做爲現有的類的一個包裝(Wrapper)。
能夠將裝飾器理解爲遊戲人物購買的裝備,例如LOL中的英雄剛開始遊戲時只有基礎的攻擊力和法強。可是在購買的裝備後,在觸發攻擊和技能時,可以享受到裝備帶來的輸出加成。咱們能夠理解爲購買的裝備給英雄的攻擊和技能的相關方法進行了裝飾。前端
這裏推薦一篇淘寶前端團隊的博文,頗有趣的以鋼鐵俠的例子來說解了裝飾者模式。node
ESnext中有一個Decorator
的提案,使用一個以 @
開頭的函數對ES6中的class
及其屬性、方法進行修飾。Decorator
的詳細語法請參考阮一峯的《ECMASciprt入門 —— Decorator》。webpack
目前Decorator
的語法還只是一個提案,若是指望如今使用裝飾器模式,須要安裝配合babel
+ webpack
並結合插件實現。git
npm install babel-core babel-loader babel-plugin-transform-decorators babel-plugin-transform-decorators-legacy babel-preset-env
.babelrc
文件{ "presets": ["env"], "plugins": ["transform-decorators-legacy"] }
webpack.config.js
中添加babel-loader
module: { rules: [ { test: /\.js$/, exclude: /node_modules/, loader: "babel-loader" } ], }
若是你使用的IDE爲Visual Studio Code,可能還須要在項目根目錄下添加如下tsconfig.json
文件來組織一個ts檢查的報錯。es6
{ "compilerOptions": { "experimentalDecorators": true, "allowJs": true, "lib": [ "es6" ], } }
下面我將實現3個裝飾器,分別爲@autobind
、@debounce
、@deprecate
。github
@autobind
實現this
指向原對象在JavaScript中,this
的指向問題一直是一個老生常談的話題,在Vue或React這類框架的使用過程當中,新手頗有可能一不當心就丟失了this
的指向致使方法調用錯誤。例以下面一段代碼:web
class Person { getPerson() { return this; } } let person = new Person(); let { getPerson } = person; console.log(getPerson() === person); // false
上面的代碼中, getPerson
方法中的this
默認指向Person
類的實例,可是若是將Person
經過解構賦值的方式提取出來,那麼此時的this
指向爲undefined
。因此最終的打印結果爲false
。ajax
此時咱們能夠實現一個autobind
的函數,用來裝飾getPerson
這個方法,實現this
永遠指向Person
的實例。npm
function autobind(target, key, descriptor) { var fn = descriptor.value; var configurable = descriptor.configurable; var enumerable = descriptor.enumerable; // 返回descriptor return { configurable: configurable, enumerable: enumerable, get: function get() { // 將該方法綁定this var boundFn = fn.bind(this); // 使用Object.defineProperty從新定義該方法 Object.defineProperty(this, key, { configurable: true, writable: true, enumerable: false, value: boundFn }) return boundFn; } } }
咱們經過bind
實現了this
的綁定,並在get
中利用Object.defineProperty
重寫了該方法,將value
定義爲經過bind
綁定後的函數boundFn
,以此實現了this
永遠指向實例。下面咱們爲getPerson
方法加上裝飾並調用。json
class Person { @autobind getPerson() { return this; } } let person = new Person(); let { getPerson } = person; console.log(getPerson() === person); // true
@debounce
實現函數防抖函數防抖(debounce)在前端項目中有着不少的應用,例如在resize
或scroll
等事件中操做DOM,或對用戶輸入實現實時ajax搜索等會被高頻的觸發,前者會對瀏覽器性能產生直觀的影響,後者會對服務器產生較大的壓力,咱們指望這類高頻連續觸發的事件在觸發結束後再作出響應,這就是函數防抖的應用。
class Editor { constructor() { this.content = ''; } updateContent(content) { console.log(content); this.content = content; // 後面有一些消耗性能的操做 } } const editor1 = new Editor(); editor1.updateContent(1); setTimeout(() => { editor1.updateContent(2); }, 400); const editor2= new Editor(); editor2.updateContent(3); setTimeout(() => { editor2.updateContent(4); }, 600); // 打印結果: 1 3 2 4
上面的代碼中咱們定義了Editor
這個類,其中updateContent
方法會在用戶輸入時執行並可能有一些消耗性能的DOM操做,這裏咱們在該方法內部打印了傳入的參數以驗證調用過程。能夠看到4次的調用結果分別爲1 3 2 4
。
下面咱們實現一個debounce
函數,該方法傳入一個數字類型的timeout
參數。
function debounce(timeout) { const instanceMap = new Map(); // 建立一個Map的數據結構,將實例化對象做爲key return function (target, key, descriptor) { return Object.assign({}, descriptor, { value: function value() { // 清除延時器 clearTimeout(instanceMap.get(this)); // 設置延時器 instanceMap.set(this, setTimeout(() => { // 調用該方法 descriptor.value.apply(this, arguments); // 將延時器設置爲 null instanceMap.set(this, null); }, timeout)); } }) } }
上面的方法中,咱們採用了ES6提供的Map
數據結構去實現實例化對象和延時器的映射。在函數的內部,首先清除延時器,接着設置延時執行函數,這是實現debounce
的通用方法,下面咱們來測試一下debounce
裝飾器。
class Editor { constructor() { this.content = ''; } @debounce(500) updateContent(content) { console.log(content); this.content = content; } } const editor1 = new Editor(); editor1.updateContent(1); setTimeout(() => { editor1.updateContent(2); }, 400); const editor2= new Editor(); editor2.updateContent(3); setTimeout(() => { editor2.updateContent(4); }, 600); //打印結果: 3 2 4
上面調用了4次updateContent
方法,打印結果爲3 2 4
。1
因爲在400ms
內被重複調用而沒有被打印,這符合咱們的參數爲500
的預期。
@deprecate
實現警告提示在使用第三方庫的過程當中,咱們會時不時的在控制檯碰見一些警告,這些警告用來提醒開發者所調用的方法會在下個版本中被棄用。這樣的一行打印信息也許咱們的常規作法是在方法內部添加一行代碼便可,這樣其實在源碼閱讀上並不友好,也不符合單一職責原則。若是在須要拋出警告的方法前面加一個@deprecate
的裝飾器來實現警告,會友好得多。
下面咱們來實現一個@deprecate
的裝飾器,其實這類的裝飾器也能夠擴展成爲打印日誌裝飾器@log
,上報信息裝飾器@fetchInfo
等。
function deprecate(deprecatedObj) { return function(target, key, descriptor) { const deprecatedInfo = deprecatedObj.info; const deprecatedUrl = deprecatedObj.url; // 警告信息 const txt = `DEPRECATION ${target.constructor.name}#${key}: ${deprecatedInfo}. ${deprecatedUrl ? 'See '+ deprecatedUrl + ' for more detail' : ''}`; return Object.assign({}, descriptor, { value: function value() { // 打印警告信息 console.warn(txt); descriptor.value.apply(this, arguments); } }) } }
上面的deprecate
函數接受一個對象參數,該參數分別有info
和url
兩個鍵值,其中info
填入警告信息,url
爲選填的詳情網頁地址。下面咱們來爲一個名爲MyLib
的庫的deprecatedMethod
方法添加該裝飾器吧!
class MyLib { @deprecate({ info: 'The methods will be deprecated in next version', url: 'http://www.baidu.com' }) deprecatedMethod(txt) { console.log(txt) } } const lib = new MyLib(); lib.deprecatedMethod('調用了一個要在下個版本被移除的方法'); // DEPRECATION MyLib#deprecatedMethod: The methods will be deprecated in next version. See http://www.baidu.com for more detail // 調用了一個要在下個版本被移除的方法
經過ESnext中的裝飾器實現裝飾器模式,不只有爲類擴充功能的做用,並且在閱讀源碼的過程當中起到了提示做用。上面所舉到的例子只是結合裝飾器的新語法和裝飾器模式作了一個簡單封裝,請勿用於生產環境。若是你如今已經體會到了裝飾器模式的好處,並想在項目中大量使用,不妨看一下core-decorators
這個庫,其中封裝了不少經常使用的裝飾器.