從ES6重新認識JavaScript設計模式: 裝飾器模式
1 什麼是裝飾器模式
向一個現有的物件新增新的功能,同時又不改變其結構的設計模式被稱為裝飾器模式(Decorator Pattern),它是作為現有的類的一個包裝(Wrapper)。
可以將裝飾器理解為遊戲人物購買的裝備,例如LOL中的英雄剛開始遊戲時只有基礎的攻擊力和法強。但是在購買的裝備後,在觸發攻擊和技能時,能夠享受到裝備帶來的輸出加成。我們可以理解為購買的裝備給英雄的攻擊和技能的相關方法進行了裝飾。
這裡推薦一篇淘寶前端團隊的博文,很有趣的以鋼鐵俠的例子來講解了裝飾者模式。
2 ESnext中的裝飾器模式
ESnext中有一個Decorator
@
開頭的函式對ES6中的class
及其屬性、方法進行修飾。Decorator
的詳細語法請參考阮一峰的《ECMASciprt入門 —— Decorator》。
目前Decorator
的語法還只是一個提案,如果期望現在使用裝飾器模式,需要安裝配合babel
+ webpack
並結合外掛實現。
- npm安裝依賴
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
{
"compilerOptions": {
"experimentalDecorators": true,
"allowJs": true,
"lib": [
"es6"
],
}
}
```
下面我將實現3個裝飾器,分別為@autobind
、@debounce
、@deprecate
。
2.1 @autobind
實現this
指向原物件
在JavaScript中,this
的指向問題一直是一個老生常談的話題,在Vue或React這類框架的使用過程中,新手很有可能一不小心就丟失了this
的指向導致方法呼叫錯誤。例如下面一段程式碼:
class Person {
getPerson() {
return this;
}
}
let person = new Person();
let { getPerson } = person;
console.log(getPerson() === person); // false
上面的程式碼中, getPerson
方法中的this
預設指向Person
類的例項,但是如果將Person
通過解構賦值的方式提取出來,那麼此時的this
指向為undefined
。所以最終的列印結果為false
。
此時我們可以實現一個autobind
的函式,用來裝飾getPerson
這個方法,實現this
永遠指向Person
的例項。
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
方法加上裝飾並呼叫。
class Person {
@autobind
getPerson() {
return this;
}
}
let person = new Person();
let { getPerson } = person;
console.log(getPerson() === person); // true
2.2 @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
的預期。
2.3 @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
// 呼叫了一個要在下個版本被移除的方法
3 總結
通過ESnext中的裝飾器實現裝飾器模式,不僅有為類擴充功能的作用,而且在閱讀原始碼的過程中起到了提示作用。上面所舉到的例子只是結合裝飾器的新語法和裝飾器模式做了一個簡單封裝,請勿用於生產環境。如果你現在已經體會到了裝飾器模式的好處,並想在專案中大量使用,不妨看一下core-decorators
這個庫,其中封裝了很多常用的裝飾器.