ES7 Decorator 應用小結

你的肖同學發表於2019-03-03

許多物件導向的語言都有 裝飾器(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)
複製程式碼

由於在非同步程式設計的時候,asyncawait的異常很難捕獲,如果強行用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`)
    }
}
複製程式碼

裝飾器載入順序

一個類或者方法可以巢狀很多個裝飾器,所以搞清楚它們的執行順序也很重要:

  • 有多個引數裝飾器時,從最後一個引數依次向前執行;
  • 方法和方法引數中引數裝飾器先執行;
  • 類裝飾器總是最後執行;
  • 方法和屬性裝飾器,誰在前面誰先執行;
  • 因為引數屬於方法一部分,所以引數會一直緊緊挨著方法執行。

裝飾器的應用

在初衷那裡就已經提到了,試著想象一下,只需要幾個裝飾器就可以完成前後端基本的效能和日誌監控,是不是很有意思?

相關文章