探索TypeScript:裝飾器
前言
最近在學習Nest.js的內容,發現裝飾器本質和Java的面向切面編程。裝飾器用于給類,方法,屬性以及方法參數等增加一些附屬功能而不影響其原有特性。其在Typescript應用中的主要作用類似于Java中的注解,在AOP(面向切面編程)使用場景下非常有用。
面向切面編程(AOP) 是一種編程范式,它允許我們分離橫切關注點,藉此達到增加模塊化程度的目標。它可以在不修改代碼自身的前提下,給已有代碼增加額外的行為(通知)
裝飾器一般用于處理一些與類以及類屬性本身無關的邏輯,例如: 一個類方法的執行耗時統計或者記錄日志,可以單獨拿出來寫成裝飾器。
看一下官方的解釋更加清晰明了
裝飾器是一種特殊類型的聲明,它能夠被附加到類聲明,方法, 訪問符,屬性或參數上。 裝飾器使用 @expression這種形式,expression求值后必須為一個函數,它會在運行時被調用,被裝飾的聲明信息做為參數傳入。
如果有使用過spring boot或者php的symfony框架的話,就基本知道裝飾器的作用分別類似于以上兩者注解和annotation,而node中裝飾器用的比較好的框架是nest.js。不過不了解也沒關系,接下來我就按我的理解講解一下裝飾器的使用。
不過目前裝飾器還不屬于標準,還在建議征集的第二階段,但這并不妨礙我們在ts中的使用。只要在 tsconfig.json中開啟 experimentalDecorators就可以使用了。
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true
}
}
類裝飾器
類裝飾器僅接受一個參數,該參數表示類本身。
同時,如果類裝飾器返回一個值,它會使用提供的構造函數來替換類的聲明。
比如:
// 類裝飾器,接受一個參數即為類本身
// 將裝飾后的類以及類的原型全部凍結變為不可擴展以及不可修改
function freeze(constructor: Function) {
Object.freeze(constructor); // 凍結裝飾的類
Object.freeze(constructor.prototype); // 凍結類的原型
}
// 調用 freeze 裝飾裝飾 BugReport
@freeze
class BugReport {
static type = 'report'
}
BugReport.type = 'hello'
console.log(BugReport.type) // TypeError: Cannot assign to read only property 'type' of function 'class BugReport
同時類裝飾器如果存在一個有效返回值,該返回值會替代被修飾類的構造函數返回的實例對象。比如:
function override(target: new () => any) {
return class Child {
}
}
@override // override 裝飾器修改了 Parent class 返回的實例對象
class Parent {
}
const instance = new Parent()
console.log(instance) // Child {}
方法裝飾器
方法裝飾器是在方法聲明之前聲明的。方式裝飾器可用于觀察、修改或替換方法定義。
方法裝飾器接受三個參數:
- 如果該裝飾器修飾的是類的靜態方法,那么第一個參數表示當前類的構造函數(即當前類)。如果修飾為類的原型方式,那么第一個參數表示該類的原型對象(prototype)。
- 第二個參數表示該方法參數器修改的類的名稱。
- 第三個參數表示當前方法的屬性描述符。
同時,如果方法裝飾器返回一個值,它會被用作方法的屬性描述符。
比如下面的例子,我們使用方法裝飾器修改類的實例方法,將 greet 方法變為不可枚舉:
function enumerable(value: boolean) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log(target) // Greeter.prototype
console.log(propertyKey) // greet
// 將該方法(Greeter.prototype.greet) 變為不可枚舉
descriptor.enumerable = value;
};
}
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
// @enumerable(false) 修飾實例方法,既修飾器第一個參數為 Greeter.prototype
@enumerable(false)
greet() {
return "Hello, " + this.greeting;
}
}
console.log(Object.keys(Greeter.prototype)) // []
屬性訪問器裝飾器
屬性訪問器裝飾器同樣在屬性訪問器聲明前使用,常用于觀察、修改或替換屬性訪問器的定義。
當屬性裝飾器被調用時,和方法裝飾器同樣會接受三個參數,分別為:
- 如果當前屬性訪問器為類的靜態屬性訪問器,那么屬性訪問器修飾器接受的第一個參數則為當前類的構造函數。否則,如果修飾的為實例上的屬性訪問器,則第一個參數為類的原型。
- 第二個參數為當前被修飾的成員名稱。
- 第三個參數為當前被修飾的屬性描述符。
同樣,如果訪問器裝飾器返回一個值,它也會被用作方法的屬性描述符。
比如,當我們使用裝飾器來修飾當前類上的屬性訪問器時:
function baseLog(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
// 觸發屬性訪問器時
console.log(`Trigger getter(${target.name}/${propertyKey})`)
}
class Person {
@baseLog
static get username() {
return '19Qingfeng'
}
}
// Trigger getter(Person/username)
// 19Qingfeng
console.log(Person.username)
參數裝飾器
同樣,class 上每個方法的參數還存在參數修飾器。參數修飾器會為參數聲明之前,同樣具有三個參數:
- 當參數修飾器修飾的所在方法為類的構造函數/靜態方法時,第一個參數表示類的構造函數(類本身)。反之,當參數修飾器修飾的參數所在的方法為實例方法時,此時第一個參數代表類的原型。
- 如果修飾的為類的靜態/實例方法時,第二個參數為當前參數修飾器所在方法的方法名。如果參數修飾器所在的方法為類的構造函數參數修飾時,此時第二個參數為 undefined。
- 第三個參數,表示當前參數所在方法的位置索引。
我們依次來看看參數裝飾器分別裝飾類的構造函數、類的靜態方法上的參數以及類的實例方法上的參數不同表現:
參數修飾器所在方法為修飾類的構造函數:
class Person {
constructor(@logger name: string) {
}
}
function logger(target: any, methodName: string | undefined, index: number) {
console.log(target) // [Function: Person]
console.log(methodName) // undefined
console.log(index) // 0
}
至此所有常見的類裝飾器都介紹完了,其實本質的裝飾器函數入參都是一致的,第一個參數是裝飾器所在的類名、第二個參數是裝飾參數,接下來我們看一下裝飾器的實現原理。
實現原理
我們將一個包含很多裝飾器的類將ts代碼編譯成es5的打包結果如下:
// ....
// 屬性裝飾器
__decorate([propertyDecorators], Parent.prototype, 'company', undefined);
// 訪問器屬性裝飾器(原型)
__decorate([accessorDecorator], Parent.prototype, 'gender', null);
// 方法裝飾器 & 參數(實例方法)裝飾器
__decorate(
[methodDecorator, __param(0, paramDecorator)],
Parent.prototype,
'getName',
null
);
// 訪問器屬性裝飾器(實例)
__decorate([accessorDecorator], Parent, 'staticGender', null);
// 方法裝飾器(實例)
__decorate([methodDecorator], Parent, 'getStaticName', null);
// 類裝飾器 & 參數裝飾器(類的構造函數)
Parent = __decorate([logger, __param(0, paramDecorator)], Parent);
return Parent;
會發現所有裝飾器都在調用__decorate方法,并且不同的裝飾器,對于__decorate方法的入參也是通用型很強。
- 第一個參數表示當前修飾器個數的集合,這是一個數組。
- 第二個參數表示當前修飾器修飾的目標(類的構造函數或者類的原型),這一步在 TS 編譯后就已經確定。
- 第三個參數如果存在的話,表示當前修飾器修飾對象的 key (這是一個字符串,可能為方法名、屬性名等)。
- 第四個參數如果存在的話,為 null 或者為 undefined。
然后我們再看一下具體的__decorate方法:
var __decorate = function (decorators, target, key, desc) {
// 首先獲得實參的個數
var c = arguments.length,
// 1. 如果實參個數小于 3 ,則表示該裝飾器為 類裝飾或者在構造函數上的參數裝飾器
// 2. 如果實參個數大于等于3, 則表示為非 1 情況的裝飾器。
// 2.1 此時根據傳入的第四個參數,來判斷是否存在屬性描述
// 如果 desc 傳入 null,則獲取當前 target key 的屬性描述符給 r 賦值。比如訪問器屬性裝飾器、方法裝飾器
// 相反如果傳入非 null (通常為 undefined), 則直接返回 desc 。比如屬性裝飾器
// 此時 r 根據不同情況,
// 要么是傳入的 target (實參個數小于3)
// 要么是 Object.getOwnPropertyDescriptor(target, key) (實參個數小于3,且 desc 為 null)
// 要么是 undefined (實參個數小于3, desc 為 undefined)
r =
c < 3
? target
: desc === null
? (desc = Object.getOwnPropertyDescriptor(target, key))
: desc,
d;
for (var i = decorators.length - 1; i >= 0; i--) {
// 從數組的末尾到首部依次遍歷獲得每一個裝飾方法
if ((d = decorators[i])) {
// 同樣判斷參數個數
// 1. 如果實參個數小于 3, 類裝飾器/構造函數上的參數裝飾
// 此時 d 為當前裝飾器方法, r 為傳入的 target (Parent)
// 此時直接使用當前裝飾器進行調用,傳入 d(r) 也就是 d(Parent)
// 2. 如果實參個數大于 3 ,則調用當前裝飾 d(target, key, r)
// 3. 如果實參個數等于 3 , 則調用 d(target, key)
// 同時為 r 重新賦值,交給下一次 for 循環遍歷處理下一個裝飾器函數
r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
}
}
// 最終裝飾器函數會進行返回
// 如果個數大于 3,并且 r 存在 則會返回 Object.defineProperty(target, key, r) ,將返回的 r 當作屬性描述符定義在 target key 上
// 最終返回 r
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
函數最后都會返回r對象,一開始會給予實參個數以及特定參數進行判斷處理,然后基于decorators、target獲得所有裝飾方法,然后拿到裝飾類的原型。
最終,會返回處理后的裝飾器方法 r,在類裝飾器上我們會使用到返回后的 r 重新賦值給當前構造函數。
Parent = __decorate([logger, __param(0, paramDecorator)], Parent);
至此,深入淺出裝飾器全過程結束。