TS语法之装饰器(注解)
写在前面的话
本文只讲解 TypeScript 中的装饰器语法(下称注解), 只会告诉你如何编写一个自定义注解,且通过注解简单的修改逻辑,不涉及 反射 或 元编程 等其他更进一步的代码讲解,如果有兴趣可以自行搜索相关的进阶写法,比如这位: TS从装饰器到注解到元编程
开始
注1:装饰器仅用于 class
语法中,所以以下举例中只会使用 class
写法。(其实是我只测试了 class
语法中的使用,而且也没必要在普通方法上使用注解)
注2:tsconfig.json
中 target
必须为 ES5
或以上版本。且 experimentalDecorators
与 emitDecoratorMetadata
字段需为 true
由于我们写的是 TypeScript
,我们首先定义几个 interface
和 type
,让 IDE 有一些基本提示:
type Prototype = {
constructor: Function
} & any
type Constructor = { new(...args: any[]): {} };
interface FunctionAnnotation {
<T>(target: Prototype, propertyKey: PropertyKey, descriptor: TypedPropertyDescriptor<T>): void;
}
interface ConstructorAnnotation {
<T extends Constructor>(constructor: T): T;
}
interface PropertyAnnotation {
(target: Prototype, propertyKey: PropertyKey): void;
}
interface ParameterAnnotation {
(target: Prototype, propertyKey: PropertyKey, parameterIndex: number): void;
}
其中,PropertyKey
和 TypedPropertyDescriptor<T>
都在 lib.es5.d.ts
文件中有定义,其值为:
declare type PropertyKey = string | number | symbol;
interface TypedPropertyDescriptor<T> {
enumerable?: boolean;
configurable?: boolean;
writable?: boolean;
value?: T;
get?: () => T;
set?: (value: T) => void;
}
如果有人不喜欢泛型写法,也可以将 TypedPropertyDescriptor<T>
替换成 PropertyDescriptor
interface PropertyDescriptor {
configurable?: boolean;
enumerable?: boolean;
value?: any;
writable?: boolean;
get?(): any;
set?(v: any): void;
}
这里使用泛型仅仅是因为我不喜欢any
,Prototype
类型中加上any
是因为没有办法只能用any
,原因是类型定义中没有办法用symbol
类型的值定义成key
,否则就可以写成type Prototype = {[key: string]: Function; [key: number]: Function; [key: symbol]: Function;}
了
如果有人不知道PropertyDescriptor
是干嘛的,请参考Object.defineProperty()
下面我们直接用例子教你如何自定义一个注解。
方法注解
写法:
export function logFuncCall(): FunctionAnnotation {
return function (target, propertyKey, descriptor) {
if (typeof descriptor.value === "function") {
let value: Function = descriptor.value;
// @ts-ignore
descriptor.value = function (...args: any) {
console.log(`${Date()} ${target.constructor.name}["${String(propertyKey)}"] be Called`);
return value.call(this, ...args);
};
}
};
}
注:若注解中需要传入参数,参考正常方法的写法添加需要的参数即可,不赘述。(注解与正常方法的区别仅仅在于方法调用前面多了一个@符号而已)
参数详解:
target
:被注解类的prototype
,本例中为Test.prototype
,如果被注解的方法是静态方法,则为类本身,也就是Test
propertyKey
:被注解的方法名,本例中为method
descriptor
:属性描述,参考Object.defineProperty(o, p, attributes)
中attributes
参数
需要注意的是,如果注解中不需要传入任何参数,则可以省略注解中最外层的一层方法,变成如下:
export <FunctionAnnotation>function logFuncCall(target, propertyKey, descriptor) {
if (typeof descriptor.value === "function") {
let value: Function = descriptor.value;
// @ts-ignore
descriptor.value = function (...args: any) {
console.log(`${target.constructor.name}["${String(propertyKey)}"] be Called`);
return value.call(this, ...args);
};
}
}
这时,注解则可以写成 @logFuncCall
的形式(去掉了括号),否则括号不能省略。下同,不再赘述。本文并未采用省略写法。
直接编译调试运行如下代码:
class Test {
@logFuncCall()
method() {}
}
let t = new Test("ttt");
t.method();
这时,控制台中会输出:
Test["method"] be Called
Getter/Setter注解
同 方法注解;
与方法注解的区别在于,Getter/Setter
注解的 descriptor
中只有对应的 get
和 set
字段,而 value
字段为 undefined
,而方法注解只中有 value
字段,而 get
和 set
字段为 undefined
所以通用的方法注解应该如下:
export function logFuncCall(): FunctionAnnotation {
return function (target, propertyKey, descriptor) {
if (typeof descriptor.value === "function") {
let value: Function = descriptor.value;
// @ts-ignore
descriptor.value = function (...args: any) {
console.log(`${target.constructor.name}["${String(propertyKey)}"] be Called`);
return value.call(this, ...args);
};
}
if (typeof descriptor.get === "function") {
let get = descriptor.get;
descriptor.get = function () {
console.log(`${target.constructor.name}["get ${String(propertyKey)}"] be Called`);
return get.call(this);
};
}
if (typeof descriptor.set === "function") {
let set = descriptor.set;
descriptor.set = function (value) {
console.log(`${target.constructor.name}["set ${String(propertyKey)}"] be Called`);
set.call(this, value);
};
}
};
}
类注解
写法:
export function logCreate(): ConstructorAnnotation {
return <T extends Constructor>(constructor: T) => {
return class extends constructor {
constructor(...args: any[]) {
console.log(`${Date()} ${constructor.name} new an instance`);
super(...args);
}
};
};
}
参数详解
constructor
:被注解类的原对象,而不是类的prototype
上面这种写法适用于大部分情况,类似于继承了被注解的类
如果你不希望任何人重写此类的方法,可以用如下的匿名写法:(在类的 constructor
方法中返回一个对象,则 new 对象时 this
指向该对象,而非类的实例)
export function logCreate(): ConstructorAnnotation {
return <T extends Constructor>(constructor: T) => {
return <T>class {
constructor(...args: any[]) {
console.log(`${Date()} ${constructor.name} new an instance`);
return new constructor(...args);
}
};
};
}
这种写法需要注意的是:环境中被注解的类将会变为一个注解返回的匿名内部类,其不继承于原先的类,在类外也无法调用原先类的所有方法,只有使用 new 新建对象后,才会返回一个原先类的对象(除非你在内部类中自己定义了静态方法去调用原先类的静态方法)
被这种写法注解的类如果被继承,则只能在构造函数中对自身做出一定修改,且所有静态方法不可通过类名调用
属性注解
写法:
export function logGetSet(): PropertyAnnotation {
return function (target, propertyKey) {
let s = "_" + String(propertyKey);
Object.defineProperty(target, propertyKey, {
get() {
console.log(`get ${String(propertyKey)}`);
return this[s];
},
set(v) {
console.log(`set ${String(propertyKey)} => ${v}`);
this[s] = v;
},
configurable: true,
enumerable: true,
});
};
}
参数详解
target
:参考方法注解propertyKey
:参考方法注解
注:propertyKey
是有可能为 symbol
或 number
类型的,所以 String()
大概率不能省略
这里需要注意的是:由于注解执行紧跟在类的定义之后,所以如果没有在定义时赋默认值,则 target
中是不会有对应的属性字段的,这时可以选择自己定义 Getter/Setter
然后在其中编写对应的方法,也可以选择在项目中引入 import 'reflect-metadata'
,使用 反射-元数据 编程(参考本文开头给出的链接 )
如果注解的是静态字段,且在定义时就赋过值,则 target
中将会有对应的属性字段且有值
参数注解
写法:
export function logParam(): ParameterAnnotation {
return function (target, propertyKey, parameterIndex) {
...
};
}
参数详解
target
:参考方法注解propertyKey
:参考方法注解parameterIndex
:被注解参数位置,从 0 开始
这里除了反射以外好像没别的用法?或者跟方法注解联动,毕竟方法也是对象,是可以保存参数的,案例:
export function logFuncCall(): FunctionAnnotation {
return <FunctionAnnotation>function (target, propertyKey, descriptor) {
if (typeof descriptor.value === "function") {
let value: Function = descriptor.value;
// @ts-ignore
descriptor.value = function (...args: any) {
// @ts-ignore
value.logParam?.forEach(i => console.log(`第${i}参数: ${args[i]}`));
return value.call(this, ...args);
};
}
}
export function logParam(): ParameterAnnotation {
return function (target, propertyKey, parameterIndex) {
target[propertyKey].logParam = [...target[propertyKey].logParam ?? [], parameterIndex];
};
}
class Test {
@logFuncCall()
set(@logParam() log: string) {}
}
new Test.set("aaa")
> 第0参数: aaa
这样同时注解 @logFuncCall()
和 @logParam
之后,就可以打印传入的参数了
结尾
另外附上一些自己写的小案例:
type Prototype = {
constructor: Function
__proto__: Prototype
} & any
/** (运行时)当父类中没有对应方法时提示报错 */
export function override(): FunctionAnnotation {
return (target, propertyKey) => {
if (target.__proto__?.[propertyKey] === undefined) {
console.error(`Method "${String(propertyKey)}" Not A Override Function`);
}
};
}
/** 自动调用父类的对应方法 */
export function autoCallSuper(): FunctionAnnotation {
return (target, propertyKey, descriptor) => {
if (typeof descriptor.value === "function") {
let value = descriptor.value;
if (typeof target.__proto__?.[propertyKey] === "function") {
// @ts-ignore
descriptor.value = function (...args: any) {
target.__proto__[propertyKey].apply(this, args);
return value.apply(this, args);
};
} else {
console.error(`${target.__proto__.constructor.name} No Function Name is "${String(propertyKey)}"`);
}
} else {
console.warn(`"autoCallSuper" Annotation Only Used On NormalFunction, Not Getter/Setter`);
}
};
}
测试案例:
@logCreate()
class A {
@logFuncCall()
a() {
console.log("A");
}
}
@logCreate()
class B extends A {
constructor() {
super();
this.a();
}
@logFuncCall()
@autoCallSuper()
a() {
console.log("B");
}
static b() {}
}
console.dir(B);
console.dir(new B());
B.b();
调试 log:
[class (anonymous) extends B] // console.dir(B);
B new an instance // new B()
A new an instance // super被调用
B["a"] be Called // 调用a方法
A["a"] be Called // 由于autoCallSuper,先调用super方法
A // super方法打印A
B // 重写方法打印B
B {} // console.dir(new B());打印new出来的对象
Function["b"] be Called // B.b();
如果类注释采用了匿名写法,则 log 为:
[class (anonymous)] // 匿名类没有继承B
B new an instance // new B()
A new an instance // super被调用,但是返回了A对象,导致this指针变为了A对象的实例
A["a"] be Called // this.a()
A // 方法打印A
A {} // console.dir(new B());打印new出来的对象
Uncaught TypeError: B.b is not a function // 匿名类没有继承B,自然也就没有B里的静态方法,报错
最后,小心溢出,不用
最后,性能太低,不用
最后,还没成真的语法呢,不用
最后,编写一时爽,debug火葬场