TypeScript: TypeScript 装饰器
最后更新:2026-08-26
装饰器(Decorators)是实验性的语法——它用 @decorator 语法给类、方法、属性添加元数据或修改行为。在框架(如 NestJS、Angular)中广泛使用。
1. 装饰器概述
(1) 什么是装饰器
装饰器是一个函数——它接收目标(类/方法/属性)作为参数,可以修改或增强目标的行为:
TYPESCRIPT
// 装饰器函数
function log(target: any, key: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`调用 ${key},参数:${args}`);
return original.apply(this, args);
};
}
class Calculator {
@log
add(a: number, b: number): number {
return a + b;
}
}
let calc = new Calculator();
calc.add(1, 2);
// 输出:调用 add,参数:1,2
(2) 开启装饰器支持
装饰器是实验性特性,需要在 tsconfig.json 中开启:
JSON
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
(3) 五种装饰器类型
| 类型 | 修饰目标 | 参数 |
|---|---|---|
| 类装饰器 | 类定义 | 构造函数 |
| 方法装饰器 | 类方法 | 目标、方法名、描述符 |
| 属性装饰器 | 类属性 | 目标、属性名 |
| 参数装饰器 | 函数参数 | 目标、方法名、参数索引 |
| 访问器装饰器 | getter/setter | 目标、访问器名、描述符 |
2. 类装饰器
类装饰器接收构造函数作为参数,可以修改或替换类定义:
(1) 基本用法
TYPESCRIPT
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
@sealed
class Config {
host: string = "localhost";
port: number = 3000;
}
// Config 已被密封——不能添加新属性
(2) 装饰器工厂
需要参数时,用工厂函数返回装饰器:
TYPESCRIPT
function className(prefix: string) {
return function (constructor: Function) {
constructor.prototype._displayName = `${prefix}_${constructor.name}`;
};
}
@className("App")
class UserService {}
// UserService.prototype._displayName = "App_UserService"
(3) 替换构造函数
TYPESCRIPT
function logged<T extends { new (...args: any[]): {} }>(constructor: T) {
return class extends constructor {
constructor(...args: any[]) {
console.log(`创建 ${constructor.name},参数:${args}`);
super(...args);
}
};
}
@logged
class User {
constructor(public name: string, public age: number) {}
}
let user = new User("Charlie", 20);
// 输出:创建 User,参数:Charlie,20
▶ 示例:注册装饰器——自动收集类
TYPESCRIPT
const registry: Map<string, any> = new Map();
function Register(name: string) {
return function <T extends { new (...args: any[]): {} }>(constructor: T) {
registry.set(name, constructor);
return constructor;
};
}
@register("user")
class UserService {
getUser(id: number) { return { id, name: "用户" + id }; }
}
@register("product")
class ProductService {
getProduct(id: number) { return { id, title: "商品" + id }; }
}
// 通过名称获取服务
function getService(name: string) {
let Service = registry.get(name);
if (!Service) throw new Error(`未注册的服务:${name}`);
return new Service();
}
let user = (getService("user") as UserService).getUser(1);
let product = (getService("product") as ProductService).getProduct(2);
console.log(user.name); // "用户1"
console.log(product.title); // "商品2"
3. 方法装饰器
方法装饰器可以观察、修改或替换方法定义:
(1) 基本用法
TYPESCRIPT
function enumerable(value: boolean) {
return function (target: any, key: string, descriptor: PropertyDescriptor) {
descriptor.enumerable = value;
};
}
class Person {
constructor(public name: string) {}
@enumerable(false)
getFullName(): string {
return this.name;
}
}
(2) 方法执行日志
TYPESCRIPT
function measure(target: any, key: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = function (...args: any[]) {
const start = performance.now();
const result = original.apply(this, args);
const end = performance.now();
console.log(`${key} 执行时间:${(end - start).toFixed(2)}ms`);
return result;
};
return descriptor;
}
class DataProcessor {
@measure
processLargeArray(data: number[]): number {
return data.reduce((sum, n) => sum + n, 0);
}
}
let processor = new DataProcessor();
processor.processLargeArray(Array.from({ length: 1000000 }, (_, i) => i));
// 输出:processLargeArray 执行时间:XX.XXms
(3) 方法防抖
TYPESCRIPT
function debounce(delay: number) {
return function (target: any, key: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
let timer: any;
descriptor.value = function (...args: any[]) {
clearTimeout(timer);
timer = setTimeout(() => original.apply(this, args), delay);
};
return descriptor;
};
}
class SearchBox {
@debounce(300)
search(query: string): void {
console.log(`搜索:${query}`);
}
}
let box = new SearchBox();
box.search("T"); // 300ms 后如果没新输入才执行
box.search("Ty");
box.search("Typ");
box.search("TypeScript"); // 只有这一次会执行
4. 属性装饰器与参数装饰器
(1) 属性装饰器
TYPESCRIPT
function format(formatStr: string) {
return function (target: any, key: string) {
// 存储格式化信息到元数据
Reflect.defineMetadata("format", formatStr, target, key);
};
}
class User {
@format("YYYY-MM-DD")
birthday: string = "2000-01-15";
@format("HH:mm:ss")
loginTime: string = "09:30:00";
}
(2) 参数装饰器
TYPESCRIPT
function required(target: any, key: string, index: number) {
const existing: number[] = Reflect.getMetadata("required", target, key) || [];
existing.push(index);
Reflect.defineMetadata("required", existing, target, key);
}
class UserService {
createUser(@required name: string, @required email: string, age?: number) {
// 参数验证在运行时由框架处理
}
}
5. 装饰器组合
多个装饰器可以同时使用——执行顺序是从下到上(靠近目标的先执行):
TYPESCRIPT
function log1(target: any, key: string, descriptor: PropertyDescriptor) {
console.log("log1 应用");
}
function log2(target: any, key: string, descriptor: PropertyDescriptor) {
console.log("log2 应用");
}
class Example {
@log1 // 第二个应用(外层)
@log2 // 第一个应用(内层,靠近方法)
method() {}
}
// 输出:log2 应用 → log1 应用
(1) 常见组合模式
TYPESCRIPT
class ApiController {
@logged
@measure
@debounce(100)
async fetchData(url: string): Promise<any> {
// 先 debounce → 再 measure → 再 logged(从下到上应用)
}
}
❓ 常见问题
Q 装饰器是正式特性吗?
A 目前还是实验性特性(Stage 3 提案阶段)。TypeScript 5.0 开始支持新的 Stage 3 装饰器语法(不再需要
experimentalDecorators),但旧语法仍然可用。NestJS、Angular 等框架目前使用旧语法。建议跟随所用框架的推荐方式。Q 装饰器能用于函数吗?
A 不能。装饰器只能用于类和类成员(方法、属性、参数)。普通函数不支持装饰器——这是 JavaScript 提案的设计限制。如果需要增强函数,用高阶函数(wrapper)模式。
Q 装饰器有什么性能影响?
A 装饰器在类定义时执行一次(不是每次调用时执行),开销很小。方法装饰器修改的包装函数每次调用都有轻微开销(多一层函数调用),但通常可以忽略。性能敏感场景避免在热路径上使用多层装饰器。
Q 不用装饰器能实现同样的效果吗?
A 可以。装饰器本质是语法糖——
@log method() 等价于 method = log(method)。不用装饰器可以用高阶函数、mixin 模式、AOP 工具库等方式实现。装饰器的优势是声明式、直观、代码更整洁。📖 小节
- 装饰器用
@decorator语法给类和类成员添加元数据或修改行为 - 五种装饰器:类、方法、属性、参数、访问器——各有不同的参数签名
- 装饰器工厂返回装饰器函数——让装饰器可以接收参数
- 类装饰器可以替换构造函数(返回新类),方法装饰器可以包装方法
- 多个装饰器从下到上应用(靠近目标的先执行)
- 装饰器是实验性特性,需要
experimentalDecorators选项
📝 作业
- 基础题(难度⭐):写一个
@readonly方法装饰器,把方法设为不可覆写(writable: false)。在一个类的方法上使用它,尝试在子类覆写该方法,观察效果。 - 进阶题(难度⭐⭐):写一个
@deprecated(message)装饰器工厂——被装饰的方法调用时打印废弃警告。提示:在方法包装函数中加console.warn。 - 挑战题(难度⭐⭐⭐):用类装饰器实现简单的依赖注入——
@Injectable()标记服务类,@Inject(Service)标记依赖属性,Container.resolve(TargetClass)自动解析并注入依赖。