裝飾器是一種特殊類型的聲明,它能夠被附加到類聲明,方法, 訪問符,屬性或參數上,可以修改類的行為。 裝飾器使用 @expression這種形式,expression求值后必須為一個函數,它會在運行時被調用,被裝飾的聲明信息做為參數傳入。
例:
@Path('/hello')
class HelloService {}
在TypeScript中裝飾器還屬於實驗性語法,所以要想使用必須在配置文件中tsconfig.json編譯選項中開啟:
{
"compilerOptions": {
"experimentalDecorators": true
}
}
如何定義裝飾器
裝飾器本身其實就是一個函數,理論上忽略參數的話,任何函數都可以當做裝飾器使用。例:
demo.ts
function Path(target:any) {
console.log("I am decorator.")
}
@Path
class HelloService {}
使用tsc編譯后,執行命令node demo.js,輸出結果如下:
I am decorator.
裝飾器執行時機
修飾器對類的行為的改變,是代碼編譯時發生的(不是TypeScript編譯,而是js在執行機中編譯階段),而不是在運行時。這意味着,修飾器能在編譯階段運行代碼。也就是說,修飾器本質就是編譯時執行的函數。
在Node.js環境中模塊一加載時就會執行
函數柯里化解決參數問題
但是實際場景中,有時希望向裝飾器傳入一些參數, 如下:
@Path("/hello", "world")
class HelloService {}
此時上面裝飾器方法就不滿足了(VSCode編譯報錯),這是我們可以借助JavaScript中函數柯里化特性
function Path(p1: string, p2: string) {
return function (target) { // 這才是真正裝飾器
// do something
}
}
五種裝飾器
在TypeScript中裝飾器可以修飾四種語句:類,屬性,訪問器,方法以及方法參數。
1 類裝飾器
應用於類構造函數,其參數是類的構造函數。
注意class並不是像Java那種強類型語言中的類,而是JavaScript構造函數的語法糖。
function Path(path: string) {
return function (target: Function) {
!target.prototype.$Meta && (target.prototype.$Meta = {})
target.prototype.$Meta.baseUrl = path;
};
}
@Path('/hello')
class HelloService {
constructor() {}
}
console.log(HelloService.prototype.$Meta);// 輸出:{ baseUrl: '/hello' }
let hello = new HelloService();
console.log(hello.$Meta) // 輸出:{ baseUrl: '/hello' }
2 方法裝飾器
它會被應用到方法的 屬性描述符上,可以用來監視,修改或者替換方法定義。
方法裝飾會在運行時傳入下列3個參數:
- 1、對於靜態成員來說是類的構造函數,對於實例成員是類的原型對象。
- 2、成員的名字。
- 3、成員的屬性描述符。
function GET(url: string) {
return function (target, methodName: string, descriptor: PropertyDescriptor) {
!target.$Meta && (target.$Meta = {});
target.$Meta[methodName] = url;
}
}
class HelloService {
constructor() { }
@GET("xx")
getUser() { }
}
console.log((<any>HelloService).$Meta);
注意:在vscode編輯時有時會報作為表達式調用時,無法解析方法修飾器的簽名。錯誤,此時需要在tsconfig.json中增加target配置項:
{
"compilerOptions": {
"target": "es6",
"experimentalDecorators": true,
}
}
3 方法參數裝飾器
參數裝飾器表達式會在運行時當作函數被調用,傳入下列3個參數:
- 1、對於靜態成員來說是類的構造函數,對於實例成員是類的原型對象。
- 2、參數的名字。
- 3、參數在函數參數列表中的索引。
function PathParam(paramName: string) {
return function (target, methodName: string, paramIndex: number) {
!target.$Meta && (target.$Meta = {});
target.$Meta[paramIndex] = paramName;
}
}
class HelloService {
constructor() { }
getUser( @PathParam("userId") userId: string) { }
}
console.log((<any>HelloService).prototype.$Meta); // {'0':'userId'}
4 屬性裝飾器
屬性裝飾器表達式會在運行時當作函數被調用,傳入下列2個參數:
- 1、對於靜態成員來說是類的構造函數,對於實例成員是類的原型對象。
- 2、成員的名字。
function DefaultValue(value: string) {
return function (target: any, propertyName: string) {
target[propertyName] = value;
}
}
class Hello {
@DefaultValue("world") greeting: string;
}
console.log(new Hello().greeting);// 輸出: world
裝飾器加載順序
function ClassDecorator() {
return function (target) {
console.log("I am class decorator");
}
}
function MethodDecorator() {
return function (target, methodName: string, descriptor: PropertyDescriptor) {
console.log("I am method decorator");
}
}
function Param1Decorator() {
return function (target, methodName: string, paramIndex: number) {
console.log("I am parameter1 decorator");
}
}
function Param2Decorator() {
return function (target, methodName: string, paramIndex: number) {
console.log("I am parameter2 decorator");
}
}
function PropertyDecorator() {
return function (target, propertyName: string) {
console.log("I am property decorator");
}
}
@ClassDecorator()
class Hello {
@PropertyDecorator()
greeting: string;
@MethodDecorator()
greet( @Param1Decorator() p1: string, @Param2Decorator() p2: string) { }
}
輸出結果:
I am property decorator
I am parameter2 decorator
I am parameter1 decorator
I am method decorator
I am class decorator
從上述例子得出如下結論:
1、有多個參數裝飾器時:從最后一個參數依次向前執行
2、方法和方法參數中參數裝飾器先執行。
3、類裝飾器總是最后執行。
4、方法和屬性裝飾器,誰在前面誰先執行。因為參數屬於方法一部分,所以參數會一直緊緊挨着方法執行。上述例子中屬性和方法調換位置,輸出如下結果:
I am parameter2 decorator
I am parameter1 decorator
I am method decorator
I am property decorator
I am class decorator
