許多物件導向的語言都有 裝飾器(Decorator) 函式,用來修改類的行為。目前,這個方法已經被引入了 ES7,但是無論是主流瀏覽器還是 Node.js 對它的相容性都不是特別友好。
因此要在專案中使用Decorator
的話,需要使用 Babel 進行轉譯,或者使用 Javascript 的超集 Typescript 來進行開發。
如果對這一語法細節還不是很瞭解的話,可以先進這個傳送門:http://es6.ruanyifeng.com/#docs/decorator ,跟著阮一峰老師一起了解一下它的特性。
初衷
使用 裝飾器 的初衷來自於不想修改原來介面的情況下,能讓一件事表現得更好。就像:
- 手機可以用,但是加了手機殼就能防摔;
- 椅子可以坐,但是墊了墊子就能夠坐的更舒服;
- 步槍可以射擊,但是加了瞄準鏡就可以射的更準;
- ……
如果要更加抽象地理解,在計算機領域,它就可以被應用到日誌收集、錯誤捕獲、安全檢查、快取、除錯、持久化等等方面。
常用的裝飾器
常用的裝飾器一般有 類裝飾器 和 方法裝飾器,當然也會有屬性裝飾器,但是用的不多就不多討論了。
類裝飾器
主要應用於類建構函式,其引數是類的建構函式:
function testable(target) {
target.prototype.isTestable = true
}
@testable
class MyTestableClass {}
let obj = new MyTestableClass()
obj.isTestable // true
複製程式碼
注意: 這裡的target
引數如果直接給它新增方法,獲得的是一個靜態方法,相當於在class
的方法前新增static
關鍵字;如果想新增例項屬性,可以通過目標類的prototype
物件操作。
現在我們就用 類裝飾器 實現一個捕獲方法執行時間的裝飾器:
const sleepTimeClass = (timeHandler?: (time?: number) => void) => (target: any) => {
Object.getOwnPropertyNames(target.prototype).forEach(key => {
const func = target.prototype[key]
target.prototype[key] = async (...args: any[]) => {
const startTime = await +new Date()
await func.apply(this, args)
const endTime = await +new Date()
timeHandler && await timeHandler(endTime - startTime)
}
})
return target
}
複製程式碼
之所以還在外面包了一層函式,是為了通過柯里化,讓使用者可以再進一步處理得到的執行時間:
const sleepTimeClassTimer = sleepTimeClass(time => {
console.log(`執行時間`, `${time}ms`)
})
@sleepTimeClassTimer
class homepageController {
async get(ctx: any) {
ctx.response.body = await pageService.homeHtml(`/page/helloworld`, `/page/404`)
}
}
複製程式碼
這樣,每次class
中的方法執行完之後就會列印出相應的執行時間。
方法裝飾器
function readonly(target, name, descriptor){
// descriptor物件原來的值如下
// {
// value: specifiedFunction,
// enumerable: false,
// configurable: true,
// writable: true
// }
descriptor.writable = false
return descriptor
}
readonly(Person.prototype, `name`, descriptor)
// 類似於
Object.defineProperty(Person.prototype, `name`, descriptor)
複製程式碼
由於在非同步程式設計的時候,async
和await
的異常很難捕獲,如果強行用try...catch
來搞,捕捉不完不說,程式碼看起來還很難看,使用裝飾器就很簡單了:
const asyncMethod = (errorHandler?: (error?: Error) => void) => (...args: any[]) => {
const func = args[2].value
return {
get() {
return (...args: any[]) => {
return Promise.resolve(func.apply(this, args)).catch(error => {
errorHandler && errorHandler(error)
})
}
},
set(newValue: any) {
return newValue
}
}
}
複製程式碼
接著使用方法裝飾器:
const errorAsyncMethod = asyncMethod(error => {
console.error(`錯誤警告`, error)
})
class homepageController {
@errorAsyncMethod async get(ctx: any) {
ctx.response.body = await pageService.homeHtml(`/page/helloworld`, `/page/404`)
}
}
複製程式碼
裝飾器載入順序
一個類或者方法可以巢狀很多個裝飾器,所以搞清楚它們的執行順序也很重要:
- 有多個引數裝飾器時,從最後一個引數依次向前執行;
- 方法和方法引數中引數裝飾器先執行;
- 類裝飾器總是最後執行;
- 方法和屬性裝飾器,誰在前面誰先執行;
- 因為引數屬於方法一部分,所以引數會一直緊緊挨著方法執行。
裝飾器的應用
在初衷那裡就已經提到了,試著想象一下,只需要幾個裝飾器就可以完成前後端基本的效能和日誌監控,是不是很有意思?