持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第14天,点击查看活动详情
TypeScript 中的装饰器(Decorator)是一种特殊语法,用于在类、方法、属性或参数上添加元数据或修改它们的行为。
装饰器本质上是一个函数,它可以在运行时被调用,并接收特定的参数来对目标进行修饰。
TypeScript 装饰器的实现基于 ECMAScript 装饰器提案,但目前 TypeScript 的实现与标准提案可能有所不同。
接下来,我会从基础到高级逐步介绍 TypeScript 中的装饰器,包括如何装饰类、方法、属性以及函数参数。
1. 装饰器基础
1.1 什么是装饰器?
装饰器是一个函数,它可以被附加到类、方法、属性或参数上,用来修改或扩展它们的行为。装饰器的语法是以 @
开头的函数调用。
1.2 启用装饰器
在 TypeScript 中使用装饰器,需要在 tsconfig.json
中启用 experimentalDecorators
选项:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
experimentalDecorators
:启用装饰器语法。emitDecoratorMetadata
:生成装饰器的元数据(可选)。
2. 类装饰器
2.1 类装饰器的定义
类装饰器是一个函数,它接收类的构造函数作为参数,可以用来修改或扩展类的行为。
当装饰器装饰类的时候,有如下规则:
- 装饰器本身是一个函数
- 装饰器接受的参数必须是一个构造函数,也就是类本身
- 装饰器通过@符号来调用
- 装饰器的执行时机是在类创建好之后立即执行,而不是在new的时候执行
2.2 类装饰器的使用
function ClassDecorator(constructor: Function) {
console.log("Class Decorator called");
// 可以在这里修改类的原型或静态属性
constructor.prototype.newProperty = "This is a new property";
}
@ClassDecorator
class MyClass {
constructor() {
console.log("MyClass instance created");
}
}
const instance = new MyClass();
console.log((instance as any).newProperty); // 输出: This is a new property
输出:
Class Decorator called
MyClass instance created
This is a new property
有的时候,我想要执行装饰器,但是有的时候我不想,那么就可以使用工厂模式来做。如下代码,我们就执行了一个函数,并且传入一个参数,根据参数来控制返回的装饰器函数。
function testDecorator(flag: boolean) {
if (flag) {
return function (constructor: any) {
constructor.prototype.getName = () => {
console.log('dell')
}
}
} else {
// 空函数,什么都不做
return function (constructor: any) {}
}
}
@testDecorator(true)
class Test {}
const test = new Test()
;(test as any).getName()
2.3 类装饰器的应用场景
- 添加元数据。
- 修改类的原型。
- 实现依赖注入。
上面演示了如何修改类的原型,这里简要讲下什么是依赖注入及如何实现。
依赖注入(Dependency Injection,简称 DI) 是一种设计模式,用于实现 控制反转(Inversion of Control,IoC) 。
它的核心思想是将对象的依赖关系由外部容器或框架来管理,而不是在对象内部直接创建依赖。
通过依赖注入,代码的耦合度降低,模块化和可测试性增强。依赖注入广泛应用于现代框架中,如 Angular、NestJS、Spring 等。
在编程中,一个类或对象可能需要依赖其他类或对象来完成某些功能。例如:
class Engine {
start() {
console.log("Engine started");
}
}
class Car {
private engine: Engine;
constructor() {
this.engine = new Engine(); // Car 依赖 Engine
}
start() {
this.engine.start();
console.log("Car started");
}
}
const car = new Car();
car.start();
在上面的代码中,Car
类直接创建了 Engine
的实例,这意味着 Car
和 Engine
是紧耦合的。
依赖注入的思想是:将依赖的创建和管理交给外部容器,而不是在类内部直接创建依赖。例如:
class Engine {
start() {
console.log("Engine started");
}
}
class Car {
private engine: Engine;
constructor(engine: Engine) { // 通过构造函数注入依赖
this.engine = engine;
}
start() {
this.engine.start();
console.log("Car started");
}
}
const engine = new Engine();
const car = new Car(engine); // 将 Engine 实例注入 Car
car.start();
在这个例子中,Car
不再直接创建 Engine
,而是通过构造函数接收 Engine
的实例。这就是依赖注入的核心思想。
依赖注入的三种方式:
1. 构造函数注入
通过构造函数传递依赖(最常用的方式)。
class Engine {
start() {
console.log("Engine started");
}
}
class Car {
constructor(private engine: Engine) {} // 构造函数注入
start() {
this.engine.start();
console.log("Car started");
}
}
const engine = new Engine();
const car = new Car(engine);
car.start();
2. 属性注入
通过属性赋值传递依赖。
class Engine {
start() {
console.log("Engine started");
}
}
class Car {
engine: Engine; // 属性注入
start() {
this.engine.start();
console.log("Car started");
}
}
const engine = new Engine();
const car = new Car();
car.engine = engine; // 设置依赖
car.start();
3. 方法注入
通过方法参数传递依赖。
class Engine {
start() {
console.log("Engine started");
}
}
class Car {
start(engine: Engine) { // 方法注入
engine.start();
console.log("Car started");
}
}
const engine = new Engine();
const car = new Car();
car.start(engine); // 传递依赖
下面看一下依赖注入在 NestJS 中的应用,NestJS 是一个基于 TypeScript 的后端框架,也广泛使用依赖注入。
import { Injectable } from '@nestjs/common';
@Injectable()
export class Engine {
start() {
console.log("Engine started");
}
}
@Injectable()
export class Car {
constructor(private readonly engine: Engine) {} // 依赖注入:构造函数注入
@Inject(JwtService) // 依赖注入:属性注入
private JwtService: JwtService
start() {
this.engine.start();
console.log("Car started");
}
}
以下是一个简单的依赖注入容器的实现:
class Container {
private instances = new Map<string, any>();
register(key: string, instance: any) {
this.instances.set(key, instance);
}
resolve<T>(key: string): T {
const instance = this.instances.get(key);
if (!instance) {
throw new Error(`No instance found for key: ${key}`);
}
return instance;
}
}
class Engine {
start() {
console.log("Engine started");
}
}
class Car {
constructor(private engine: Engine) {}
start() {
this.engine.start();
console.log("Car started");
}
}
使用容器管理依赖
const container = new Container();
container.register("engine", new Engine());
container.register("car", new Car(container.resolve("engine")));
实例的创建都是容器创建的
const car = container.resolve<Car>("car");
car.start();
3. 方法装饰器
3.1 方法装饰器的定义
首先要强调装饰器对普通的方法无效,只能对类里面的方法有效
方法装饰器是一个函数,它接收以下参数:
- 第一个参数:如果是通用方法,target 对应的是类的 prototype,如果是静态方法static,对应的是类的构造函数
- 第二个参数:就是装饰的方法名
- 第三个参数:类似于Object.defineProperty的descriptor,可以设置属性是否可写,可读等操作
3.2 方法装饰器的使用
function MethodDecorator(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
console.log("Method Decorator called");
console.log("Target:", target);
console.log("Property Key:", propertyKey);
console.log("Descriptor:", descriptor);
// 修改方法的行为
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Method ${propertyKey} is called with args:`, args);
const result = originalMethod.apply(this, args);
console.log(`Method ${propertyKey} returned:`, result);
return result;
};
}
class MyClass {
@MethodDecorator
myMethod(arg1: string, arg2: number) {
console.log("Executing myMethod");
return `Received: ${arg1}, ${arg2}`;
}
}
const instance = new MyClass();
instance.myMethod("Hello", 42);
输出:
Method Decorator called
Target: MyClass { myMethod: [Function] }
Property Key: myMethod
Descriptor: {
value: [Function],
writable: true,
enumerable: true,
configurable: true
}
Method myMethod is called with args: [ 'Hello', 42 ]
Executing myMethod
Method myMethod returned: Received: Hello, 42
3.3 方法装饰器的应用场景
- 日志记录。
- 性能监控。
- 方法拦截(如权限检查)。
首先看一个日志记录的场景:
class Test {
userInfo: any = undefined
getName() {
return this.userInfo.name
}
getAge() {
return this.userInfo.age
}
}
const test = new Test()
test.getName()
test.getAge()
当访问getName或者getAge的时候,就会报错,因为userInfo是undefined,那我们修改下:
class Test {
userInfo: any = undefined
getName() {
try {
return this.userInfo.name
} catch (err) {
console.log(err)
}
}
getAge() {
try {
return this.userInfo.age
} catch (err) {
console.log(err)
}
}
}
如果有很多这种方法,那么我们就要写很多try catch,这样代码就变得非常的长,这个时候就可以使用装饰器了。
function catchError(target: any, key: string, descriptor: PropertyDescriptor) {
const fn = descriptor.value
descriptor.value = function () {
try {
fn()
} catch (e) {
console.log('userInfo 存在问题')
}
}
}
class Test {
userInfo: any = undefined
@catchError
getName() {
return this.userInfo.name
}
@catchError
getAge() {
return this.userInfo.age
}
}
通过装饰器,就可以节省大量的代码,但是还有个问题,报错信息要从外部传进去,那怎么做呢?
function catchError(msg: string) {
return function (target: any, key: string, descriptor: PropertyDescriptor) {
const fn = descriptor.value
descriptor.value = function () {
try {
fn()
} catch (e) {
console.log(msg)
}
}
}
}
class Test {
userInfo: any = undefined
@catchError('userInfo.name 不存在')
getName() {
return this.userInfo.name
}
@catchError('userInfo.age 不存在')
getAge() {
return this.userInfo.age
}
@catchError('userInfo.gender 不存在')
getGender() {
return this.userInfo.gender
}
}
再来看看方法装饰器是如何用于实现方法拦截(Method Interception)。
简单来说,是在方法执行前后插入额外的逻辑,比如权限检查、缓存、日志记录等。通过方法装饰器,我们可以轻松地拦截方法的调用,并在必要时修改方法的行为。
function Intercept(interceptor: { before?: Function; after?: Function }) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
// 保存原始方法
const originalMethod = descriptor.value;
// 重写方法
descriptor.value = function (...args: any[]) {
// 执行前置拦截逻辑
if (interceptor.before) {
interceptor.before(args);
}
// 调用原始方法
const result = originalMethod.apply(this, args);
// 执行后置拦截逻辑
if (interceptor.after) {
interceptor.after(result);
}
// 返回原始方法的结果
return result;
};
return descriptor;
};
}
class MyClass {
@Intercept({
before: (args: any[]) => {
console.log(`[BEFORE] Method is about to be called with args: ${JSON.stringify(args)}`);
},
after: (result: any) => {
console.log(`[AFTER] Method returned: ${JSON.stringify(result)}`);
},
})
myMethod(arg1: string, arg2: number) {
console.log("Executing myMethod");
return `Received: ${arg1}, ${arg2}`;
}
}
const instance = new MyClass();
instance.myMethod("Hello", 42);
运行上述代码后,控制台会输出以下日志:
[BEFORE] Method is about to be called with args: ["Hello",42]
Executing myMethod
[AFTER] Method returned: "Received: Hello, 42"
4. 属性装饰器
4.1 属性装饰器的定义
属性装饰器是一个函数,它接收以下参数:
- 类的原型(如果是静态属性,则是类的构造函数)。
- 属性名称。
4.2 属性装饰器的使用
function PropertyDecorator(target: any, propertyKey: string) {
console.log("Property Decorator called");
console.log("Target:", target);
console.log("Property Key:", propertyKey);
// 可以在这里修改属性的行为
let value: any;
const getter = () => {
console.log(`Getting value: ${value}`);
return value;
};
const setter = (newValue: any) => {
console.log(`Setting value: ${newValue}`);
value = newValue;
};
// 替换属性的 getter 和 setter
Object.defineProperty(target, propertyKey, {
get: getter,
set: setter,
enumerable: true,
configurable: true,
});
}
class MyClass {
@PropertyDecorator
myProperty: string;
}
const instance = new MyClass();
instance.myProperty = "Hello";
console.log(instance.myProperty);
输出:
Property Decorator called
Target: MyClass {}
Property Key: myProperty
Setting value: Hello
Getting value: Hello
Hello
4.3 属性装饰器的应用场景
- 属性验证。
- 属性监听(如 Vue 的响应式系统)。
通过在属性赋值时触发验证逻辑,我们可以确保属性的值符合特定的规则或约束。
以下是一个简单的实现,展示如何使用属性装饰器实现属性验证:
function Validate(validationFn: (value: any) => boolean) {
return function (target: any, propertyKey: string) {
let value: any;
// 定义属性的 getter 和 setter
const getter = () => value;
const setter = (newValue: any) => {
// 执行验证逻辑
if (!validationFn(newValue)) {
throw new Error(`Invalid value for property ${propertyKey}: ${newValue}`);
}
value = newValue;
};
// 替换属性的 getter 和 setter
Object.defineProperty(target, propertyKey, {
get: getter,
set: setter,
enumerable: true,
configurable: true,
});
};
}
class MyClass {
@Validate((value: number) => value > 0) // 验证值必须大于 0
myProperty: number;
}
const instance = new MyClass();
// 合法赋值
instance.myProperty = 42;
console.log(instance.myProperty); // 输出: 42
// 非法赋值
try {
instance.myProperty = -1; // 抛出错误
} catch (error) {
console.error(error.message); // 输出: Invalid value for property myProperty: -1
}
5. 参数装饰器
5.1 参数装饰器的定义
参数装饰器是一个函数,它接收以下参数:
- 类的原型(如果是静态方法,则是类的构造函数)。
- 方法名称。
- 参数在函数参数列表中的索引。
5.2 参数装饰器的使用
function ParameterDecorator(target: any, methodName: string, parameterIndex: number) {
console.log("Parameter Decorator called");
console.log("Target:", target);
console.log("Method Name:", methodName);
console.log("Parameter Index:", parameterIndex);
}
class MyClass {
myMethod(@ParameterDecorator arg1: string, arg2: number) {
console.log(`Executing myMethod with args: ${arg1}, ${arg2}`);
}
}
const instance = new MyClass();
instance.myMethod("Hello", 42);
输出:
Parameter Decorator called
Target: MyClass { myMethod: [Function] }
Method Name: myMethod
Parameter Index: 0
Executing myMethod with args: Hello, 42
5.3 参数装饰器的应用场景
- 参数验证。
- 依赖注入。
下面来看一个例子,参数装饰器如何实现依赖注入的。
import "reflect-metadata";
// 定义参数装饰器
function Inject(token: string) {
return function (target: any, methodName: string, parameterIndex: number) {
Reflect.defineMetadata("inject", token, target, `${methodName}_${parameterIndex}`);
};
}
// 定义服务类
class MyService {
doSomething() {
console.log("MyService is doing something");
}
}
// 定义需要注入的类
class MyClass {
constructor(@Inject("MyService") private myService: MyService) {}
doSomething() {
this.myService.doSomething();
}
}
定义依赖注入容器
class Container {
private instances = new Map<string, any>();
register(token: string, instance: any) {
this.instances.set(token, instance);
}
resolve<T>(token: string): T {
const instance = this.instances.get(token);
if (!instance) {
throw new Error(`No instance found for token: ${token}`);
}
return instance;
}
inject<T>(target: any): T {
const instance = new target();
在 TypeScript 里,`design:paramtypes` 是一种由 TypeScript 编译器自动生成的元数据,
借助反射机制,它能让我们在运行时获取构造函数参数的类型。
这里会返回构造函数参数类型数组,即[MyService]
const paramTypes = Reflect.getMetadata("design:paramtypes", target) || [];
for (let i = 0; i < paramTypes.length; i++) {
const token = Reflect.getMetadata("inject", target.prototype, `constructor_${i}`);
if (token) {
const dependency = this.resolve(token);
把MyService实例赋值给MyClass的属性myService上,这样就完成了依赖注入
instance[i] = dependency;
}
}
return instance;
}
}
// 使用依赖注入
const container = new Container();
container.register("MyService", new MyService());
// 通过container.inject实例化
const myClassInstance = container.inject(MyClass);
myClassInstance.doSomething(); // 输出: MyService is doing something
可以看到参数装饰器@Inject("MyService")
通过元数据添加了token
,在实例化MyClass
时,通过Reflect.getMetadata
取出元数据,并获取元数据对应实例MyService
,在MyClass
实例化过程中赋值给MyClass
中的myService
属性。
6. 装饰器的执行顺序
在 TypeScript 中,装饰器的执行顺序是一个非常重要的概念,尤其是当多个装饰器同时应用于类、方法、属性或参数时。
-
- 同一目标的多个装饰器
如果有多个装饰器应用于同一个目标(如类、方法、属性等),它们的执行顺序是从下到上(从最接近目标的装饰器开始)。
@DecoratorA
@DecoratorB
class MyClass {}
2. 不同类型的装饰器
如果类、方法、属性、参数等都有装饰器,它们的执行顺序是:
- 参数装饰器(Parameter Decorators)。
- 方法装饰器(Method Decorators)。
- 属性装饰器(Property Decorators)。
- 类装饰器(Class Decorators)。
function ClassDecorator(target: Function) {
console.log("Class Decorator applied");
}
function MethodDecorator(target: any, key: string, descriptor: PropertyDescriptor) {
console.log("Method Decorator applied");
}
function PropertyDecorator(target: any, key: string) {
console.log("Property Decorator applied");
}
function ParameterDecorator(target: any, key: string, parameterIndex: number) {
console.log("Parameter Decorator applied");
}
@ClassDecorator
class MyClass {
@PropertyDecorator
myProperty: string;
@MethodDecorator
myMethod(@ParameterDecorator param: string) {}
}
// 输出:
// Parameter Decorator applied
// Method Decorator applied
// Property Decorator applied
// Class Decorator applied