總目錄
從C#到TypeScript - 裝飾器
在C#里面如果想要不直接修改類或方法,但給類或方法添加一些額外的信息或功能,可以想到用Attribute
,這是一個十分方便的功能裝飾器。
用TypeScript同樣也可以利用裝飾器來給類、函數、屬性以及參數添加附加功能,裝飾器是ES7的一個提案,在TypeScript里已經有實現可用,不過需要在tsconfig.json
里啟用experimentalDecorators
。
"compilerOptions": {
..., // other options
"experimentalDecorators": true
}
裝飾器介紹
TypeScript中裝飾器可以應用到類、方法、屬性及函數參數上,而且可以同時應用多個。
裝飾器的寫法是@name()
,()
可以不要,也可以在里面寫一些參數。
@Testable
@Log('controller')
class Controller{
@GET
getContent(@QueryParam arg: string): string{
return '';
}
}
裝飾器的實現
裝飾器根據實現可以分兩種:
一種是不帶括號,和屬性一樣,如@Testable
。
function Testable(target: Function) { // 類、方法、屬性、方法參數的參數各不相同
//這里可以記錄一些信息到target,或者針對target做一些處理,如seal
}
另外一種是帶括號的,和函數一樣,如@Log('controller')
,實現函數里的參數就是括號里的參數,而且需要返回一個function
。
function Log(name: string) { // name就是傳進來的參數'controller'
return function(target: Function) { // 類、方法、屬性、方法參數的參數各不相同
// 這里可以根據name和target來做一些處理
}
}
類裝飾器
上面的(target: Function)
其實就是類的裝飾器參數,指向的是類的構造函數,如果想給類加一個簡單的seal功能,可以這樣做:
function sealed(target: Function) {
Object.seal(target);
Object.seal(target.prototype);
}
@sealed
class Test{
}
Test.prototype.test = ''; // 運行時出錯,不能添加
上面的sealed
就是類的裝飾器,target
指構造函數,類裝飾器就這么一個參數。
方法裝飾器
方法裝飾器的使用方法和類裝飾器類似,只是參數不一樣,方法裝飾器有三個參數:
- 如果裝飾的是靜態方法,則是類的構造函數,如果是實例方法則是類的原型。
- 方法的名字。
- 方法的
PropertyDescriptor
。
PropertyDescriptor
即屬性描述符,有
configurable 是否可以配置,如動態添加刪除函數屬性之類
writable 是否可寫,可以用來設置只讀屬性
enumerable 是否可枚舉,即是否能在for...in
中能枚舉到
value 對象或屬性的值
有了這些參數就可以很好的給方法添加一些功能,比如下面實現類型WebApi里的Get的路由:
const Router = Symbol(); // 唯一key,用來存裝飾器的信息
function GET(path?: string) { // GET帶了個可選參數
return (target: any, name: string) => setMethodDecorator(target, name, 'GET', path);
}
//把method和path存起來,路由查找的時候就可以用了
function setMethodDecorator(target: any, name: string, method: string, path?: string){
target[Router] = target[Router] || {};
target[Router][name] = target[Router][name] || {};
target[Router][name].method = method;
target[Router][name].path = path;
}
// 通過PropertyDescriptor來設置enumerable
function Enumerable(enumerable: boolean) {
return (target: any, name: string, descriptor: PropertyDescriptor) => {
descriptor.enumerable = enumerable;
};
}
class Controller{
@GET
@Enumerable(true)
getContent(arg: string): string{
return '';
}
}
參數裝飾器
方法參數同樣可以有裝飾器,同樣有三個參數,前兩個參數和方法的一致,最后一個參數是所裝飾的參數的位置。
能過參數裝飾器可以給方法動態的檢查或設置參數值,下面是檢查參數是否為空,為空則拋出異常。
const CheckNullKey = Symbol();
const Router = Symbol();
// 把CheckNull裝飾的參數存起來
function CheckNull(target: any, name: string, index: number) {
target[Router] = target[Router] || {};
target[Router][name] = target[Router][name] || {};
target[Router][name].params = target[Router][name].params || [];
target[Router][name].params[index] = CheckNullKey;
}
// 找出CheckNull的參數,並檢查參數值,為空則拋異常,否則繼續執行方法
function Check(target: any, name: string, descriptor: PropertyDescriptor) {
let method = descriptor.value;
descriptor.value = function () {
let params = target[Router][name].params;
if (params) {
for (let index = 0; index < params.length; index++) {
if (params[index] == CheckNullKey && // 找到CheckNull的參數並拋異常
(arguments[index] === undefined || arguments[index] === null)) {
throw new Error("Missing required argument.");
}
}
}
return method.apply(this, arguments);
}
}
class Controller{
@Check
getContent(@CheckNull id: string): string{
console.info(id);
return id;
}
}
new Controller().getContent(null); // error : Missing required argument.
屬性裝飾器
用法同上,參數只有兩個,和類裝飾器的前兩個一樣,常用來標識屬性的特性。
function Column(target: any, name: string) {
//把name存起來,這個column僅僅是標識出來對應數據庫中的列,常用在ORM框架中
}
class Table{
@Column
name: string;
}
另外還有屬性訪問器的裝飾器,和方法基本一樣,同樣的三個參數,不過同個屬性的get
和set
只能有一個有,而且必須是先聲明的那個。
class User {
private _name: string;
@Enumerable(true)
get name(){
return this._name;
}
set name(value: string) {
this._name = value;
}
}
多個裝飾器的執行順序
一個聲明可以添加多個裝飾器,所以會有個執行先后順序。
首先從上到下執行裝飾器函數,然后再從下往上應用帶括號的裝飾器返回的函數。
function Test1(){
console.info('eval test1');
return function(target: any, name: string, descriptor: PropertyDescriptor){
console.info('apply test1');
}
}
function Test2(){
console.info('eval test2');
return function(target: any, name: string, descriptor: PropertyDescriptor){
console.info('apply test2');
}
}
class User1{
@test1()
@Test2()
getName(){
}
}
結果是:
eval test1
eval test2
apply test2
apply test1
總之,裝飾器等於引入了天然的裝飾模式,給類,方法等添加額外功能。不過裝飾器目前還不算太穩定,但是由於確實方便,已經有成熟項目在使用了。