一、装饰器运行环境
装饰器是一项实验性特性,在未来的版本中可能会发生改变。
若要启用实验性的装饰器特性,你必须在命令行或tsconfig.json
里启用experimentalDecorators
编译器选项:
- tsconfig.json:
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true
}
}
二、什么是装饰器
TypeScript中的装饰器是一种可以附加到类、方法、属性或参数上的特殊声明。装饰器提供了一种在编译时检测类型和运行时修改行为的机制,在许多框架中得到了广泛的应用。
它们本质上是函数,可以接收一个或多个参数,并返回新的目标对象。
装饰器是为元素(类、方法、属性、参数)添加注解的语法糖,提供了一种简洁优雅的方式来描述元素的特性。当我们在使用装饰器时,它实际上是在目标元素周围创建一个包装器函数,以便在目标元素被调用或使用时运行一些代码。
在编译时,TypeScript编译器将自动将装饰器转换为底层JavaScript代码。在编译后的JavaScript代码中,装饰器被转换为一系列函数调用,这些函数执行了被装饰元素的包装器函数,并返回了新的包装器函数。
通过使用装饰器,我们可以在不修改被装饰元素代码的情况下,动态地添加或修改其行为。这使得我们可以在运行时或编译时优化代码、实现依赖注入、添加日志或验证等功能。
三、装饰器扩展类的属性
装饰器可以用来扩展类的属性,例如添加额外的元数据信息或者添加一些行为。下面我们以添加元数据信息为例进行说明:
function logClass(target: any) {
// 保存类构造函数原型对象引用
const original = target.prototype;
// 重新定义构造函数
const constructor = function (...args) {
console.log(`Creating instance with arguments: ${args}`);
// 调用原来的构造函数
original.constructor.apply(this, args);
};
// 给构造函数添加元数据
Object.defineProperty(constructor, 'name', { value: target.name });
Object.defineProperty(constructor, 'description', { value: 'This is a decorated class' });
// 重新定义构造函数原型对象
constructor.prototype = original;
// 返回新的构造函数
return constructor;
}
@logClass
class MyClass {
constructor(public x: number, public y: number) {}
}
const myObj = new MyClass(1, 2);
console.log(myObj.constructor.name); // 输出"MyClass"
console.log(myObj.constructor.description); // 输出"This is a decorated class"
在上面的代码中,我们定义了一个logClass
装饰器函数,它接受一个类的构造函数作为参数,并返回一个新的类构造函数。在这个装饰器函数里,我们保存了原来的类构造函数原型对象引用,并重新定义了新的构造函数。在重新定义构造函数的时候,我们给它添加了一些元数据信息,在这个例子中是类的名字和一个描述信息。最后我们再把原来的构造函数原型对象重新赋值给新的构造函数原型对象,并返回这个新的构造函数。
我们使用@logClass
装饰器修饰MyClass
类,这样就会在类定义的时候调用logClass
函数,并将MyClass
类的构造函数作为参数传递进去。logClass
函数会把类的构造函数进行处理,并在元数据信息中添加上我们定义的内容。
当我们创建了一个MyClass
的实例对象后,我们可以通过访问它的构造函数的name
和description
属性来获取到我们刚才添加的元数据信息。这个例子中我们添加的只是一些简单的元数据信息,但是在实际开发中,我们可以利用装饰器添加更加复杂的元数据信息和行为。
四、装饰器工厂
装饰器工厂是TypeScript中一种用于创建装饰器的方法,它返回一个具体的装饰器函数,在装饰器修饰的类、属性、方法或参数上添加附加的元数据或行为。
装饰器工厂的本质是一个函数,它可以接收参数,并返回一个装饰器函数。装饰器函数可以用来修改或增强原有的类、属性、方法或参数,实现各种创造性的功能。
在实际开发中,装饰器工厂可以用于很多场景,如:
- 实现日志、权限、缓存等功能的统一处理;
- 用于依赖注入,在编译时自动注入依赖对象,避免手动管理依赖关系;
- 实现类似于Spring AOP的横向切面编程,实现方法执行前、执行后、异常处理等等;
下面举例说明一个简单的装饰器工厂:
function log(className: string) {
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`[${className}] ${propertyKey} is called with arguments ${args.join(', ')}`);
const result = originalMethod.apply(this, args);
console.log(`[${className}] ${propertyKey} returned ${result}`);
return result;
}
return descriptor;
}
}
这个装饰器工厂定义了一个log方法,它返回一个装饰器函数。装饰器函数用于打印方法调用的日志。
我们可以在类的方法上使用这个装饰器工厂,如:
class Calculator {
@log('Calculator')
add(a: number, b: number) {
return a + b;
}
}
此时,当我们调用Calculator类的add方法时,就会输出日志,如:
const calculator = new Calculator();
calculator.add(1, 2);
// 输出:[Calculator] add is called with arguments 1, 2
// 输出:[Calculator] add returned 3
这样就实现了一个简单的装饰器工厂,利用它可以实现更加复杂和高级的功能。
五、属性装饰器
在TypeScript中,属性装饰器是装饰类的属性的一种方式。下面详细介绍一下属性装饰器的使用及其参数含义。
属性装饰器的使用
属性装饰器是指装饰一个类中的属性,即为类的属性添加一些元数据,通常用来定义属性的属性描述符或为属性添加一些额外行为,例如实现属性计算等。
属性装饰器使用@
符号和一个函数表示,类似于方法装饰器。
class MyClass{
@myDecor // <-- 这里是属性装饰器
myProp: string;
}
function myDecor(target: any, propertyKey: string) {
// 这里可以对属性进行修改或设置元数据
}
属性装饰器的参数含义
属性装饰器函数有两个参数,分别是target
和propertyKey
。在装饰实例属性、静态属性、实例方法时,参数所代表的含义是有所不同的,下面我们分别举例介绍。
1. 装饰实例属性
装饰实例属性时,target
表示类的原型对象(即类的实例化对象的__proto__
属性),propertyKey
表示被装饰的属性名。
class MyClass {
@myDecorator
myProp: string;
}
function myDecorator(target: any, propertyKey: string) {
console.log(target); // MyClass 的原型对象
console.log(target.constructor); // MyClass
console.log(propertyKey); // 'myProp'
}
在实例化对象后,装饰器会被应用于该实例中的属性。
const myInstance = new MyClass();
console.log(myInstance.myProp); // undefined
2. 装饰静态属性
装饰静态属性时,target
表示的是类的构造函数本身,propertyKey
表示被装饰的属性名。
class MyClass {
@myDecorator
static myStaticProp: string;
}
function myDecorator(target: any, propertyKey: string) {
console.log(target); // MyClass 构造函数本身
console.log(target.constructor); // MyClass
console.log(propertyKey); // 'myStaticProp'
}
在访问静态属性时,装饰器会被应用于该类本身。
console.log(MyClass.myStaticProp); // undefined
3. 装饰实例方法
当属性装饰器装饰实例时,装饰器的参数包括三个:
- target:被装饰的类的实例(即构造函数的原型对象)。
- propertyKey:被装饰的属性的名称。
- descriptor:被装饰属性的属性描述符(Object.defineProperty中的描述符对象)。
举个例子,假设我们有一个User类,其中有一个属性age表示用户的年龄。我们可以使用属性装饰器保证age属性的取值范围在0到120之间,代码如下:
function rangeValidator(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalSetter = descriptor.set;
descriptor.set = function (value: number) {
if (value < 0 || value > 120) {
throw new Error('Invalid age value!');
}
originalSetter.call(this, value);
};
}
class User {
private _age: number;
@rangeValidator
public get age(): number {
return this._age;
}
public set age(age: number) {
this._age = age;
}
}
const user = new User();
user.age = 25; // OK
user.age = 200; // Error: Invalid age value!
在这个例子中,rangeValidator是一个属性装饰器函数,它接收三个参数:target、propertyKey和descriptor。我们在装饰器函数中重写了age属性的setter方法,并在新的setter方法中添加了取值范围的验证逻辑。当我们使用@rangeValidator装饰age属性时,TypeScript会自动把User类传递给rangeValidator函数的target参数,把age属性的名称"age"传递给propertyKey参数,把age属性的属性描述符传递给descriptor参数。通过这些参数,我们就可以得到被装饰的类、属性和属性的描述对象,并对它们进行一些操作。
六、参数装饰器
TypeScript中的参数装饰器是一种特殊类型的装饰器,它可以用于装饰函数或方法中的参数。参数装饰器可以提供对函数或方法中传入参数的处理和校验功能。
在参数装饰器中,通常会接收三个参数:target、methodName和paramIndex。
- target:表示被装饰的类的原型对象(静态成员)或类的构造函数(非静态成员)。如果装饰的是静态成员,则target是类的构造函数;如果装饰的是非静态成员,则target是类的原型对象。
- methodName:表示被装饰的方法的名称。
- paramIndex:表示被装饰的参数在函数或方法的参数列表中的索引位置。
下面分别举例说明:
1. 装饰静态成员时的应用
在以下例子中,参数装饰器用于校验对象的id属性是否为有效值,如果无效,则抛出异常。
class Person {
static instances = new Map<number, Person>();
constructor(public id: number) {
Person.instances.set(id, this);
}
static getPersonById(@IsValidId id: number) {
return Person.instances.get(id);
}
}
function IsValidId(target: any, methodName: string, paramIndex: number) {
let originalMethod = target[methodName];
target[methodName] = function (...args: any[]) {
let id = args[paramIndex];
if (!Number.isInteger(id) || id <= 0 || id >= 1000) {
throw new Error("Invalid id");
}
return originalMethod.apply(this, args);
}
}
2. 装饰非静态成员时的应用
在以下例子中,参数装饰器用于记录函数执行时间。
class Demo {
log(@LogTime message: string) {
console.log(message);
}
}
function LogTime(target: any, methodName: string, paramIndex: number) {
let originalMethod = target[methodName];
target[methodName] = function (...args: any[]) {
console.time(`${methodName}_${paramIndex}`);
let result = originalMethod.apply(this, args);
console.timeEnd(`${methodName}_${paramIndex}`);
return result;
}
}
const demo = new Demo();
demo.log("start"); // 输出:start, start_0: 0.653ms
需要注意的是,参数装饰器并不会修改方法签名,因此需要在使用参数装饰器时,保持方法签名不变。
七、装饰器执行顺序
TypeScript中装饰器的执行顺序规律:
- 对于一个类,先执行类中属性的装饰器,然后是方法的装饰器,最后是类的装饰器。即装饰器的执行顺序为:
属性装饰器 -> 方法装饰器 -> 类装饰器
其中,后写的装饰器会先执行,也就是先执行最后一个装饰器,然后依次向前执行其他装饰器。
- 对于类中的方法和方法参数,装饰器的执行顺序为:
方法参数装饰器 -> 方法装饰器
即先执行方法参数的装饰器,然后再执行方法的装饰器。
- 对于同一属性或方法,如果有多个装饰器,它们的执行顺序由它们在代码中的顺序来决定。即先写的装饰器会先执行。
举例如下:
function classDecorator1(target: any) {
console.log("classDecorator1");
}
function classDecorator2(target: any) {
console.log("classDecorator2");
}
function methodDecorator(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log("methodDecorator");
}
function paramDecorator(target: Object, propertyKey: string, parameterIndex: number) {
console.log("paramDecorator");
}
@classDecorator1
@classDecorator2
class MyClass {
@propertyDecorator
myProperty: number;
@methodDecorator
myMethod(@paramDecorator param1: string, @paramDecorator param2: number) {
console.log("MyClass.myMethod");
}
}
对于上面的代码,执行结果为:
paramDecorator
paramDecorator
methodDecorator
propertyDecorator
classDecorator2
classDecorator1
因为参数装饰器是最先执行的,所以先输出了参数装饰器的内容;接着是方法装饰器和属性装饰器;最后是类装饰器,按照后写的装饰器会先执行的原则,先执行classDecorator2再执行classDecorator1。
总结:
先上后下 先内后外
- 属性和方法:先上后下
- 整体原则:先内后外