一、什么是装饰器?
TypeScript
中的装饰器是一种特殊类型的声明,它可以被附加到 类声明、方法、访问器、属性或参数 上,从而在不修改原始类定义的情况下,动态地修改或增强类和类成员 的功能。装饰器本质上是一个函数,它在运行时被调用,接收一些特定的参数(如目标类的构造函数、属性描述符等),并返回一个修改后的版本或者原始版本。
1. 装饰器的语法
在语法上,装饰器有如下几个特征。
-
第一个字符(或者说前缀)是
@
,后面是一个表达式 -
@
后面的表达式,必须是一个函数(或者执行后可以得到一个函数)。 -
这个函数接受所修饰对象的一些相关值作为参数。
-
这个函数要么不返回值,要么返回一个新对象取代所修饰的目标对象。
装饰器有多种形式,基本上只要在 @
符号后面添加表达式都是可以的。下面都是合法的装饰器。
@myFunc @myFuncFactory(arg1, arg2) @libraryModule.prop @someObj.method(123) @(wrap(dict['prop']))
注意,@
后面的表达式,最终执行后得到的应该是一个函数。 相比使用子类改变父类,装饰器更加简洁优雅,缺点是不那么直观,功能也受到一些限制。所以,装饰器一般只用来为类添加某种特定行为。
2. 一个简单的装饰器
// typescript function logClz(value : any, constructor : any) { console.log(`create a instance of ${constructor.name}`); } @logClz class Student { constructor() { console.log('Student constructor called'); } } const stone = new Student(); // 输出: // create a instance of Student // Student constructor called
类 Student
在执行前会先执行装饰器 logClz
,并且会向装饰器自动传入参数。
3. 装饰器的版本
TypeScript
从早期开始,就支持装饰器。但是,装饰器的语法后来发生了变化。ECMAScript
标准委员会最终通过的语法标准,与 TypeScript
早期使用的语法有很大差异。
目前,TypeScript 5.0
同时支持两种装饰器语法。标准语法可以直接使用,传统语法需要打开 --experimentalDecorators
编译参数。
> tsc --target ES5 --experimentalDecorators
这里只谈标准语法。
4. 装饰器的结构
装饰器函数的类型定义如下。
type Decorator = ( value : DecoratedValue, context : { kind : string; name : string | symbol; addInitializer?(initializer: () => void): void; static?: boolean; private?: boolean; access: { get?(): unknown; set?(value: unknown): void; }; } ) => void | ReplacementValue;
上面代码中,Decorator
是装饰器的类型定义。它是一个函数,使用时会接收到 value
和 context
两个参数。
-
value
:所装饰的对象。 -
context
:上下文对象,TypeScript
提供一个原生接口ClassMethodDecoratorContext
,描述这个对象。
function decorator( value : any, context : ClassMethodDecoratorContext ){ // ... }
上面是一个装饰器函数,其中第二个参数 context
的类型就可以写成 ClassMethodDecoratorContext
。
context
对象的属性,根据所装饰对象的不同而不同,其中只有两个属性(kind
和 name
)是必有的,其他都是可选的。
-
kind
:字符串,表示所装饰对象的类型,可能取以下的值,这表示一共有六种类型的装饰器。-
class
-
field
-
method
-
getter
-
setter
-
accessor
-
-
name
:字符串或者Symbol
值,所装饰对象的名字,比如类名、属性名等。 -
addInitializer()
:函数,用来添加类的初始化逻辑。以前,这些逻辑通常放在构造函数里面,对方法进行初始化,现在改成以函数形式传入addInitializer()
方法。注意,addInitializer()
没有返回值。 -
private
:布尔值,表示所装饰的对象是否为类的私有成员。 -
static
:布尔值,表示所装饰的对象是否为类的静态成员。 -
access
:一个对象,包含了某个值的get
和set
方法。
二、类装饰器
类装饰器在类声明之前声明,用于修改类的行为或添加额外的职责。它们接收类的构造函数作为参数,并可以修改类定义或者返回一个新的类来替换原始类。
1. 类装饰器的类型描述
type ClassDecorator = ( value : Function, context: { kind: 'class'; name: string | undefined; addInitializer(initializer: () => void): void; } ) => Function | void;
类装饰器接受两个参数:value
(当前类本身)和 context
(上下文对象)。其中,context
对象的 kind
属性固定为字符串 class
。
2. 类装饰器一般用来对类进行操作,可以不返回任何值
请看下面的例子:
function Greeter(value, context){ if(context.kind === 'class'){ value.prototype.greet = function(){ console.log("Hello, ", this.name , "!"); } } } @Greeter class User{ name : string; constructor(name : string){ this.name = name; } } const lao8 = new User('老八'); lao8['greet'](); // 必须这样调用,否则,无法调用类中的方法
上面示例中,类装饰器 @Greeter
在类 User
的原型对象上,添加了一个 greet()
方法,实例就可以直接使用该方法。
3. 类装饰器可以返回一个函数,替代当前类的构造方法
function countInstances(value, context) { let instanceCount = 0; let wrapper = function (...args: any[]) { let instance = new value(...args); instanceCount++; value.prototype.count = instanceCount; return instance; } as typeof value; wrapper.prototype = value.prototype; // A return wrapper; } @countInstances class User { } const lao8 = new User(); console.log(lao8['count']); // 1 const stone = new User(); console.log(lao8['count']); // 2 console.log(lao8 instanceof User); // true
上面示例中,类装饰器 @countInstances
返回一个函数,替换了类 User
的构造方法。新的构造方法实现了实例的计数,每新建一个实例,计数器就会加一,并且在原型中添加 count
属性,表示实例的个数。
注意,上例为了确保新构造方法继承定义在 User
的原型之上的成员,特别加入行 A
,确保两者的原型对象是一致的。否则,新的构造函数 wrapper
的原型对象,与 User
不同,通不过 instanceof
运算符。
4. 类装饰器也可以返回一个新的类,替代原来所装饰的类
function countInstances(value, context) { let instanceCount = 0; return class clz extends value { constructor(...args){ super(args); instanceCount++; value.prototype.count = instanceCount; } }; } @countInstances class User { } const lao8 = new User(); console.log(lao8['count']); // 1 const stone = new User(); console.log(lao8['count']); // 2 console.log(lao8 instanceof User); // true
上面示例中,@countInstances
返回一个 User
的子类。
5. addInitializer()
方法
类装饰器的上下文对象 context
的 addInitializer()
方法,用来定义一个类的初始化函数,在类完全定义结束 后执行。
function clzInit(value : Function, context : any) : any{ context.addInitializer(() => { console.log('initialized'); }); } @clzInit class User { constructor(){ console.log('User()'); } } let lao8 = new User(); let stone = new User();
上面示例中,类 User
定义完成后,会自动执行类装饰器 @clzInit
给出的初始化函数。
三、方法装饰器
方法装饰器在方法声明之前声明,可以修改方法的行为或添加额外的操作。
1. 方法装饰器类型描述
type ClassMethodDecorator = ( value: Function, context: { kind: 'method'; name: string | symbol; static: boolean; private: boolean; access: {get: () => unknown}; addInitializer(initializer: () => void): void; } ) => Function | void;
根据上面的类型,方法装饰器是一个函数,接受两个参数:value
和context
。</p>
参数`value`是方法本身,参数`context`是上下文对象,有以下属性。
- `kind`:值固定为字符串`method`,表示当前为方法装饰器。
- `name`:所装饰的方法名,类型为字符串或 Symbol 值。
- `static`:布尔值,表示是否为静态方法。该属性为只读属性。
- `private`:布尔值,表示是否为私有方法。该属性为只读属性。
- `access`:对象,包含了方法的存取器,但是只有`get()`方法用来取值,没有`set()`方法进行赋值。
- `addInitializer()`:为方法增加初始化函数。
2. 基本用法
方法装饰器会改写类的原始方法,实质等同于下面的操作。
function trace(value : Function, context : any) : any{ console.log('trace()'); return value; } class C{ @trace toString(){ return 'C'; } } let c = new C(); console.log(c.toString()); // `@trace` 等价于 // C.prototype.toString = trace(C.prototype.toString);
上面示例中,@trace
是方法toString()
的装饰器,它的效果等同于最后一行对toString()
的改写。</p>
3. 返回函数
如果方法装饰器返回一个新的函数,就会替代所装饰的原始函数。
function replaceMethod(value : Function, context : ClassMethodDecoratorContext) : (() => string) | void { return function(){ return `How are you, ${this.name}?`; } } class Person { name : string; constructor(name){ this.name = name; } @replaceMethod hello(){ return `Hi ${this.name}!`; } } const robin = new Person('Robin'); console.log(robin.hello()); // 'How are you, Robin?'
上面示例中,装饰器@replaceMethod
返回的函数,就成为了新的hello()
方法。</p>
4. 利用方法装饰器实现代理方法
class Person{ name: string; constructor(name: string){ this.name = name; } @log greet(){ console.log(`Hello, my name is ${this.name}.`); } } function log(originalMethod: any, context: ClassMethodDecoratorContext){ const methodName = String(context.name); function replacementMethod(this: any, ...args: any[]){ console.log(`LOG: Entering method '${methodName}'.`); const result = originalMethod.call(this, ...args); console.log(`LOG: Exiting method '${methodName}'.`); return result; } return replacementMethod; } const person = new Person('老八'); person.greet();
上面示例中,装饰器@log
的返回值是一个函数replacementMethod
,替代了原始方法greet()
。在replacementMethod()
内部,通过执行originalMethod.call()
完成了对原始方法的调用。</p>
5. 利用方法装饰器,延迟方法的执行
function delay(milliseconds: number = 0){ return function(value, context){ if(context.kind === 'method') { return function(...args: any[]){ setTimeout(() => { vlaue.apply(this, args); }, milliseconds); }; } }; } class Logger{ @delay(1000) log(msg: string){ console.log(`${msg}`); } } let logger = new Logger(); logger.log('Hello, world');
上面示例中,方法装饰器@delay(1000)
将方法log()
的执行推迟了1秒(1000毫秒)。这里真正的方法装饰器,是delay()
执行后返回的函数,delay()
的作用是接收参数,用来设置推迟执行的时间。这种通过高阶函数返回装饰器的做法,称为“工厂模式”,即可以像工厂那样生产出一个模子的装饰器。</p>
6. context.addInitializer()
方法装饰器的参数`context`对象里面,有一个`addInitializer()`方法。它是一个钩子方法,用来在类的初始化阶段,添加回调函数。这个回调函数就是作为`addInitializer()`的参数传入的,它会 **在构造方法执行期间执行**,早于属性(`field`)的初始化。
下面一个例子,通过`addInitializer()`将选定的方法名,放入一个集合。
function collect( value, {name, addInitializer} ){ addInitializer(function(){ if(!this.collectMethodKeys){ this.collectMethodKeys = new Set(); } this.collectMethodKeys.add(name); }); } class C{ @collect toString(){} @collect [Symbol.iterator](){} } const instance = new C(); console.log(instance['collectMethodKeys'])
上面示例中,方法装饰器@collect
会将所装饰的成员名字,加入一个 Set 集合collectedMethodKeys
。
四、属性装饰器
1. 类型描述
属性装饰器用来装饰定义在类顶部的属性(field)。它的类型描述如下。
type ClassFieldDecorator = { value : undefined, context : { kind : 'field', name : string | symbol, static : boolean, private : boolean, access : {get : () => unknown, set : (value : unknown) => void}, addInitializer : (initializer : () => void) => void } } => (initialValue : unknown) => unknown | void;
注意,装饰器的第一个参数value
的类型是undefined
,这意味着这个参数实际上没用的,装饰器不能从value
获取所装饰属性的值。另外,第二个参数context
对象的kind
属性的值为字符串field
,而不是 “property” 或 “attribute”,这一点是需要注意的。</p>
2. 基本使用方式
属性装饰器要么不返回值,要么返回一个函数,该函数会自动执行,用来对所装饰属性进行初始化。该函数的参数是所装饰属性的初始值,该函数的返回值是该属性的最终值。
function logged(value, context){ const {kind, name } = context; if(kind === 'field'){ return function (initialValue){ console.log(`initializing ${name} with value ${initialValue}`); return initialValue; } } } class Color { @logged name = 'green'; } const color = new Color(); // initializing name with value green
上面示例中,属性装饰器@logged
装饰属性name
。@logged
的返回值是一个函数,该函数用来对属性name
进行初始化,它的参数initialValue
就是属性name
的初始值green
。新建实例对象color
时,该函数会自动执行。
2. 属性装饰器的返回值函数,可以用来更改属性的初始值
function twice(value : any, context : any){ return initialValue => initialValue * 2; } class C { @twice field: number = 3; } const c = new C(); console.log(c.field); // 6
上面示例中,属性装饰器@twice
返回一个函数,该函数的返回值是属性field
的初始值乘以 2
,所以属性field
的最终值是 6
。</p>
3. 属性装饰器的上下文对象 context
context.access
属性,提供所装饰属性的存取器,请看下面的例子。
let acc; function exposeAccess(value, {access}) { acc = access; } class Color { @exposeAccess name = 'green'; } const green = new Color(); console.log(green.name); // green console.log(acc.get(green)); // green acc.set(green, 'red'); console.log(green.name); // red
上面示例中,access
包含了属性name
的存取器,可以对该属性进行取值和赋值。
五、getter/setter 装饰器
getter
装饰器和 setter
装饰器,是分别针对类的取值器(getter
)和存值器(setter
)的装饰器。
1. 类型描述
type ClassGetterDecorator = ( value : Function, context : { kind : 'getter'; name : string | symbol; static : boolean; private : boolean; access : {get: () => unknown}; addInitializer(initializer: () => void): void; } ) => Function | void; type ClassSetterDecorator = ( value: Function, context : { kind : 'setter'; name : string | symbol; static : boolean; private : boolean; access : {set: (value: unknown) => void}; addInitializer(initializer: () => void): void; } ) => Function | void;
注意,getter
装饰器的上下文对象 context
的 access
属性,只包含 get()
方法;setter
装饰器的 access
属性,只包含 set()
方法。
这两个装饰器要么不返回值,要么返回一个函数,取代原来的取值器或存值器。
2. 基本用法
下面的例子是将取值器的结果,保存为一个属性,加快后面的读取。
class C { @lazy get value() { console.log('正在计算...'); return '开销大的计算结果'; } } function lazy(value: any, { kind, name }: any) { if (kind === 'getter') { return function (this: any) { const result = value.call(this); Object.defineProperty( this, name, { value: result, writable: false } // 定义同名属性且不可修改 ); return result; }; } return; } const instance = new C(); console.log(instance.value) // 正在计算... // 开销大的计算结果 console.log(instance.value); // 开销大的计算结果
上面示例中,第一次读取instance.value
,会进行计算,然后装饰器@lazy
将结果存入只读属性value
,后面再读取这个属性,就不会进行计算了。
六、accessor 装饰器
1. accessor 关键字
装饰器语法(ES2015
)引入了一个新的属性修饰符accessor
。
class C { accessor x = 1; } var c = new C(); console.log(c.x); c.x = 2; console.log(c.x);
上面示例中,accessor
修饰符等同于为公开属性x
自动生成取值器和存值器,它们作用于私有属性x
。(注意,公开的x
与私有的x
不是同一个属性。)也就是说,上面的代码等同于下面的代码。
class C { #x = 1; get x() { return this.#x; } set x(value) { this.#x = value; } } // 使用命令 tsc --target ES2022 文件名.ts 编译
accessor
也可以与静态属性和私有属性一起使用。
class C { static accessor x = 1; accessor #y = 2; }
2. accessor 装饰器的类型
accessor
装饰器的类型如下:
type ClassAutoAccessorDecorator = ( value : { get: () => unknown; set: (value: unknown) => void; }, context: { kind : 'accessor'; name : string | symbol; access : {get(): unknown; set(value: unknown): void}; static : boolean; private : boolean; addInitializer(initializer: () => void): void; } ) => { get?: () => unknown; set?: (value: unknown) => void; init?: (initialValue: unknown) => unknown; } | void;
accessor 装饰器的value
参数,是一个包含get()
方法和set()
方法的对象。该装饰器可以不返回值,或者返回一个新的对象,用来取代原来的get()
方法和set()
方法。此外,装饰器返回的对象还可以包括一个init()
方法,用来改变私有属性的初始值。</p>
3. 一般应用
下面是一个例子。
class C { @logged accessor x = 1; } function logged(value, {kind, name}) { if(kind === 'accessor') { let {get, set} = value; return { get() { console.log(`getting ${name}`); return get.call(this); }, set(value){ console.log(`setting ${name} to ${value}`); return set.call(this, value); }, init(initialValue){ console.log(`initializing ${name} with value ${initialValue}`); return initialValue; } } } } let c = new C(); console.log(c.x); // getting x // 1 c.x = 2; // setting x to 2 console.log(c.x); // getting x // 2
上面示例中,装饰器@logged
为属性x
的存值器和取值器,加上了日志输出。
七、装饰器的执行顺序
装饰器的执行分为两个阶段。
-
评估(evaluation):计算
@
符号后面的表达式的值,得到的应该是函数。 -
应用(application):将评估装饰器后得到的函数,应用于所装饰对象。
也就是说,装饰器的执行顺序是,先评估所有装饰器表达式的值,再将其应用于当前类。
应用装饰器时,顺序依次为方法装饰器和属性装饰器,然后是类装饰器。 请看下面的例子:
function d(str : string) { console.log(`评估 @d(): ${str}`); return (value: any, context: any) => console.log(`应用 @d(): ${str}`); } function log(str: string){ console.log(str); return str; } @d('类装饰器') class T { @d('静态属性装饰器') static staticField = log('静态属性值'); @d('原型方法') [log('计算方法名')](){} @d('实例属性') instanceField = log('实例属性'); @d('静态方法装饰器') static fn(){} }
上面示例中,类T
有四种装饰器:类装饰器、静态属性装饰器、方法装饰器、属性装饰器。</p>
它的运行结果如下。
评估 @d(): 类装饰器 评估 @d(): 静态属性装饰器 评估 @d(): 原型方法 计算方法名 评估 @d(): 实例属性 评估 @d(): 静态方法装饰器 应用 @d(): 静态方法装饰器 应用 @d(): 原型方法 应用 @d(): 静态属性装饰器 应用 @d(): 实例属性 应用 @d(): 类装饰器 静态属性值
可以看到,类载入的时候,代码按照以下顺序执行。
-
装饰器评估:这一步计算装饰器的值,首先是类装饰器,然后是类内部的装饰器,按照它们出现的顺序。注意,如果属性名或方法名是计算值(本例是“计算方法名”),则它们在对应的装饰器评估之后,也会进行自身的评估。
-
装饰器应用:实际执行装饰器函数,将它们与对应的方法和属性进行结合。静态方法装饰器首先应用,然后是原型方法的装饰器和静态属性装饰器,接下来是实例属性装饰器,最后是类装饰器。注意,“实例属性值”在类初始化的阶段并不执行,直到类实例化时才会执行。
如果一个方法或属性有多个装饰器,则内层的装饰器先执行,外层的装饰器后执行。
class Person { name: string; constructor(name: string) { this.name = name; } @bound @log greet(){ console.log(`Hello, ${this.name}!`); } }
上面示例中,greet()
有两个装饰器,内层的@log
先执行,外层的@bound
针对得到的结果再执行。