装饰器
问题
什么是 TypeScript 装饰器?如何使用类装饰器、方法装饰器、属性装饰器和参数装饰器?
答案
装饰器是一种特殊的声明,可以附加到类、方法、属性或参数上,用于修改或增强它们的行为。装饰器是一种元编程技术,在框架中广泛使用(如 Angular、NestJS)。
注意
TypeScript 5.0 引入了新的装饰器语法(ECMAScript 标准),与之前的实验性装饰器(experimentalDecorators)不同。本文主要介绍标准装饰器和实验性装饰器。
启用装饰器
tsconfig.json
{
"compilerOptions": {
// 实验性装饰器(TypeScript 4.x 及之前)
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
// TypeScript 5.0+ 标准装饰器无需配置
}
}
类装饰器
基本用法
// 实验性装饰器
function Logger(constructor: Function) {
console.log('Creating instance of:', constructor.name);
}
@Logger
class User {
constructor(public name: string) {}
}
// 输出: "Creating instance of: User"
const user = new User('Alice');
装饰器工厂
function Logger(prefix: string) {
return function (constructor: Function) {
console.log(`${prefix}: ${constructor.name}`);
};
}
@Logger('CLASS')
class User {
name = 'Alice';
}
// 输出: "CLASS: User"
修改类
function Sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
function AddTimestamp<T extends { new (...args: any[]): object }>(
constructor: T
) {
return class extends constructor {
createdAt = new Date();
};
}
@Sealed
@AddTimestamp
class User {
constructor(public name: string) {}
}
const user = new User('Alice');
console.log((user as any).createdAt); // 当前时间
方法装饰器
基本用法
function Log(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Calling ${propertyKey} with:`, args);
const result = originalMethod.apply(this, args);
console.log(`Result:`, result);
return result;
};
return descriptor;
}
class Calculator {
@Log
add(a: number, b: number): number {
return a + b;
}
}
const calc = new Calculator();
calc.add(2, 3);
// 输出:
// Calling add with: [2, 3]
// Result: 5
缓存装饰器
function Memoize(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const cache = new Map<string, any>();
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
const key = JSON.stringify(args);
if (cache.has(key)) {
console.log('Cache hit');
return cache.get(key);
}
const result = originalMethod.apply(this, args);
cache.set(key, result);
return result;
};
return descriptor;
}
class DataService {
@Memoize
fetchData(id: number): string {
console.log('Fetching...');
return `Data for ${id}`;
}
}
const service = new DataService();
service.fetchData(1); // Fetching...
service.fetchData(1); // Cache hit
错误处理装饰器
function CatchError(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = async function (...args: any[]) {
try {
return await originalMethod.apply(this, args);
} catch (error) {
console.error(`Error in ${propertyKey}:`, error);
throw error;
}
};
return descriptor;
}
class API {
@CatchError
async fetchUser(id: number) {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) throw new Error('Failed to fetch');
return response.json();
}
}
属性装饰器
function MinLength(length: number) {
return function (target: any, propertyKey: string) {
let value: string;
const getter = () => value;
const setter = (newValue: string) => {
if (newValue.length < length) {
throw new Error(
`${propertyKey} must be at least ${length} characters`
);
}
value = newValue;
};
Object.defineProperty(target, propertyKey, {
get: getter,
set: setter,
enumerable: true,
configurable: true
});
};
}
class User {
@MinLength(3)
name!: string;
}
const user = new User();
user.name = 'Al'; // 错误:name must be at least 3 characters
user.name = 'Alice'; // OK
响应式属性
function Observable(target: any, propertyKey: string) {
const privateKey = `_${propertyKey}`;
const listeners: Array<(value: any) => void> = [];
Object.defineProperty(target, propertyKey, {
get() {
return this[privateKey];
},
set(value) {
this[privateKey] = value;
listeners.forEach((listener) => listener(value));
}
});
target[`on${propertyKey.charAt(0).toUpperCase() + propertyKey.slice(1)}Change`] =
(callback: (value: any) => void) => {
listeners.push(callback);
};
}
class Store {
@Observable
count = 0;
}
const store = new Store() as any;
store.onCountChange((value: number) => console.log('Count changed:', value));
store.count = 5; // Count changed: 5
参数装饰器
const requiredParams: Map<Function, number[]> = new Map();
function Required(
target: any,
propertyKey: string,
parameterIndex: number
) {
const existing = requiredParams.get(target[propertyKey]) || [];
existing.push(parameterIndex);
requiredParams.set(target[propertyKey], existing);
}
function Validate(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
const required = requiredParams.get(originalMethod) || [];
for (const index of required) {
if (args[index] === undefined || args[index] === null) {
throw new Error(`Parameter at index ${index} is required`);
}
}
return originalMethod.apply(this, args);
};
return descriptor;
}
class UserService {
@Validate
createUser(@Required name: string, age?: number) {
return { name, age };
}
}
const service = new UserService();
service.createUser('Alice', 25); // OK
service.createUser(null as any); // 错误:Parameter at index 0 is required
TypeScript 5.0+ 标准装饰器
类装饰器
type ClassDecorator = <T extends new (...args: any[]) => any>(
target: T,
context: ClassDecoratorContext<T>
) => T | void;
function Logged<T extends new (...args: any[]) => any>(
target: T,
context: ClassDecoratorContext<T>
) {
console.log(`Creating class: ${context.name}`);
return target;
}
@Logged
class User {
name = 'Alice';
}
方法装饰器
type MethodDecorator = <T extends (...args: any[]) => any>(
target: T,
context: ClassMethodDecoratorContext
) => T | void;
function LogMethod<T extends (...args: any[]) => any>(
target: T,
context: ClassMethodDecoratorContext
): T {
const methodName = String(context.name);
function replacementMethod(this: any, ...args: any[]): any {
console.log(`Calling ${methodName} with args:`, args);
const result = target.call(this, ...args);
console.log(`${methodName} returned:`, result);
return result;
}
return replacementMethod as T;
}
class Calculator {
@LogMethod
add(a: number, b: number): number {
return a + b;
}
}
字段装饰器
function DefaultValue<T>(value: T) {
return function (
target: undefined,
context: ClassFieldDecoratorContext
) {
return function (initialValue: T): T {
return initialValue ?? value;
};
};
}
class Settings {
@DefaultValue('guest')
username!: string;
@DefaultValue(8080)
port!: number;
}
const settings = new Settings();
console.log(settings.username); // 'guest'
console.log(settings.port); // 8080
Accessor 装饰器
function Logged<T, V>(
target: ClassAccessorDecoratorTarget<T, V>,
context: ClassAccessorDecoratorContext<T, V>
): ClassAccessorDecoratorResult<T, V> {
return {
get(this: T): V {
console.log(`Getting ${String(context.name)}`);
return target.get.call(this);
},
set(this: T, value: V): void {
console.log(`Setting ${String(context.name)} to`, value);
target.set.call(this, value);
}
};
}
class User {
@Logged
accessor name = 'Alice';
}
装饰器组合
装饰器从上到下求值,从下到上执行:
function First() {
console.log('First(): evaluated');
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
console.log('First(): called');
};
}
function Second() {
console.log('Second(): evaluated');
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
console.log('Second(): called');
};
}
class Example {
@First()
@Second()
method() {}
}
// 输出:
// First(): evaluated
// Second(): evaluated
// Second(): called
// First(): called
实际应用场景
依赖注入
const container = new Map<string, any>();
function Injectable(token: string) {
return function (constructor: Function) {
container.set(token, new (constructor as any)());
};
}
function Inject(token: string) {
return function (target: any, propertyKey: string) {
Object.defineProperty(target, propertyKey, {
get: () => container.get(token),
enumerable: true
});
};
}
@Injectable('userService')
class UserService {
getUser() {
return { name: 'Alice' };
}
}
class Controller {
@Inject('userService')
userService!: UserService;
handleRequest() {
return this.userService.getUser();
}
}
const controller = new Controller();
console.log(controller.handleRequest()); // { name: 'Alice' }
路由装饰器
const routes: Array<{ method: string; path: string; handler: Function }> = [];
function Controller(basePath: string) {
return function (constructor: Function) {
(constructor as any).basePath = basePath;
};
}
function Get(path: string) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
routes.push({
method: 'GET',
path: path,
handler: descriptor.value
});
};
}
function Post(path: string) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
routes.push({
method: 'POST',
path: path,
handler: descriptor.value
});
};
}
@Controller('/users')
class UserController {
@Get('/')
getUsers() {
return ['Alice', 'Bob'];
}
@Post('/')
createUser() {
return { created: true };
}
}
console.log(routes);
// [
// { method: 'GET', path: '/', handler: [Function] },
// { method: 'POST', path: '/', handler: [Function] }
// ]
常见面试问题
Q1: 装饰器的执行顺序是什么?
答案:
function ClassDec() {
console.log('ClassDec: evaluated');
return (constructor: Function) => console.log('ClassDec: executed');
}
function MethodDec() {
console.log('MethodDec: evaluated');
return (t: any, k: string, d: PropertyDescriptor) =>
console.log('MethodDec: executed');
}
function PropDec() {
console.log('PropDec: evaluated');
return (t: any, k: string) => console.log('PropDec: executed');
}
function ParamDec() {
console.log('ParamDec: evaluated');
return (t: any, k: string, i: number) => console.log('ParamDec: executed');
}
@ClassDec()
class Example {
@PropDec()
prop = 1;
@MethodDec()
method(@ParamDec() arg: string) {}
}
// 执行顺序:
// 1. 属性装饰器:PropDec evaluated -> PropDec executed
// 2. 参数装饰器:ParamDec evaluated -> ParamDec executed
// 3. 方法装饰器:MethodDec evaluated -> MethodDec executed
// 4. 类装饰器:ClassDec evaluated -> ClassDec executed
// 多个装饰器:从上到下求值,从下到上执行
Q2: 实验性装饰器和标准装饰器的区别?
答案:
| 特性 | 实验性装饰器 | 标准装饰器 (TS 5.0+) |
|---|---|---|
| 配置 | 需要 experimentalDecorators | 无需配置 |
| 参数 | (target, key, descriptor) | (target, context) |
| 元数据 | 支持 emitDecoratorMetadata | 不支持 |
| 参数装饰器 | 支持 | 不支持 |
| Accessor | 不支持 | 支持 accessor 关键字 |
| 规范 | 旧提案 | ECMAScript 标准 |
// 实验性装饰器
function OldLog(
target: any,
key: string,
descriptor: PropertyDescriptor
) {
// target: 类的原型
// key: 方法名
// descriptor: 属性描述符
}
// 标准装饰器
function NewLog<T extends (...args: any[]) => any>(
target: T,
context: ClassMethodDecoratorContext
): T {
// target: 方法本身
// context: 包含名称、类型等元数据
return target;
}
Q3: 如何用装饰器实现方法执行时间统计?
答案:
// 实验性装饰器版本
function Timing(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = async function (...args: any[]) {
const start = performance.now();
const result = await originalMethod.apply(this, args);
const end = performance.now();
console.log(`${propertyKey} took ${(end - start).toFixed(2)}ms`);
return result;
};
return descriptor;
}
// 标准装饰器版本
function TimingNew<T extends (...args: any[]) => any>(
target: T,
context: ClassMethodDecoratorContext
): T {
const methodName = String(context.name);
async function replacement(this: any, ...args: any[]): Promise<any> {
const start = performance.now();
const result = await target.apply(this, args);
const end = performance.now();
console.log(`${methodName} took ${(end - start).toFixed(2)}ms`);
return result;
}
return replacement as T;
}
class DataService {
@Timing
async fetchData(id: number) {
await new Promise((r) => setTimeout(r, 100));
return { id };
}
}
Q4: 装饰器能改变类型吗?
答案:
// 装饰器可以修改运行时行为,但 TypeScript 类型不会自动更新
function AddCreatedAt<T extends new (...args: any[]) => object>(
constructor: T
) {
return class extends constructor {
createdAt = new Date();
};
}
@AddCreatedAt
class User {
name = 'Alice';
}
const user = new User();
// user.createdAt; // 运行时存在,但 TypeScript 不知道
// 解决方案 1:类型断言
console.log((user as any).createdAt);
// 解决方案 2:接口合并
interface User {
createdAt: Date;
}
@AddCreatedAt
class User {
name = 'Alice';
}
// 解决方案 3:返回新类型的工厂函数
function WithTimestamp<T extends new (...args: any[]) => object>(Base: T) {
return class extends Base {
createdAt = new Date();
};
}
class BaseUser {
name = 'Alice';
}
const User = WithTimestamp(BaseUser);
const user2 = new User();
console.log(user2.createdAt); // 类型正确
Q5: TC39 Stage 3 装饰器和 TypeScript 实验性装饰器有什么区别?
答案:
TC39 Stage 3 装饰器(TypeScript 5.0+ 原生支持)和 TypeScript 实验性装饰器(experimentalDecorators)是两套完全不兼容的方案,核心区别如下:
| 特性 | 实验性装饰器 | TC39 标准装饰器 |
|---|---|---|
| 配置 | 需要 experimentalDecorators: true | 无需配置,TS 5.0+ 默认支持 |
| 方法装饰器参数 | (target, key, descriptor) 三个参数 | (target, context) 两个参数 |
context 对象 | 无 | 包含 name、kind、addInitializer 等 |
| 参数装饰器 | 支持 | 不支持 |
accessor 关键字 | 不支持 | 支持(自动生成 getter/setter) |
| 元数据 | emitDecoratorMetadata + reflect-metadata | 原生 Symbol.metadata(提案中) |
| 执行时机 | 类定义时 | 类定义时,但 addInitializer 可注册延迟执行 |
| 互相兼容 | - | 不兼容,不能混用 |
方法装饰器对比:
// ====== 实验性装饰器 ======
// tsconfig: "experimentalDecorators": true
function OldLog(
target: any, // 类的原型对象
propertyKey: string, // 方法名
descriptor: PropertyDescriptor // 属性描述符
): PropertyDescriptor {
const original = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`[old] calling ${propertyKey}`);
return original.apply(this, args);
};
return descriptor;
}
// ====== TC39 标准装饰器 ======
// 无需额外配置
function NewLog<T extends (...args: any[]) => any>(
target: T, // 方法本身
context: ClassMethodDecoratorContext // 上下文对象
): T {
const methodName = String(context.name);
function replacement(this: any, ...args: any[]): any {
console.log(`[new] calling ${methodName}`);
return target.call(this, ...args);
}
return replacement as T;
}
addInitializer —— 标准装饰器的新能力:
function Bound<T extends (...args: any[]) => any>(
target: T,
context: ClassMethodDecoratorContext
): void {
const methodName = context.name;
// addInitializer 在类实例化时执行
context.addInitializer(function (this: any) {
// 自动绑定 this
this[methodName] = this[methodName].bind(this);
});
}
class Button {
label = 'Click me';
@Bound
handleClick() {
console.log(this.label); // 无论怎么调用,this 都正确
}
}
const btn = new Button();
const handler = btn.handleClick;
handler(); // 'Click me'(不会丢失 this)
元数据对比:
// 实验性装饰器:需要 reflect-metadata 库
import 'reflect-metadata';
function Type(target: any, key: string) {
const type = Reflect.getMetadata('design:type', target, key);
console.log(`${key} type: ${type.name}`);
}
// TC39 标准装饰器:使用原生 Symbol.metadata(提案阶段)
function Meta(key: string, value: unknown) {
return function (
target: any,
context: ClassFieldDecoratorContext
) {
// 通过 context.metadata 存储元数据
context.metadata[key] = value;
};
}
class User {
@Meta('label', '用户名')
name = '';
}
// 读取元数据
console.log(User[Symbol.metadata]); // { label: '用户名' }
注意
- 如果项目使用 Angular / NestJS,目前仍需开启
experimentalDecorators,因为这些框架尚未完全迁移到标准装饰器 - 新项目建议使用 TC39 标准装饰器
- 两种装饰器语法不能混用,开启
experimentalDecorators后 TS 会按旧语义解析所有装饰器
Q6: 如何用装饰器实现一个方法缓存(Memoize)?
答案:
方法缓存装饰器通过拦截方法调用、用参数序列化为 key 存储结果,下次相同参数直接返回缓存。进阶版还可以支持 TTL 过期策略。
基础版(实验性装饰器):
function Memoize(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
): PropertyDescriptor {
const originalMethod = descriptor.value;
// 使用 WeakMap 绑定实例,避免不同实例共享缓存
const instanceCache = new WeakMap<object, Map<string, unknown>>();
descriptor.value = function (this: object, ...args: unknown[]) {
if (!instanceCache.has(this)) {
instanceCache.set(this, new Map());
}
const cache = instanceCache.get(this)!;
// 参数序列化为 key
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key);
}
const result = originalMethod.apply(this, args);
cache.set(key, result);
return result;
};
return descriptor;
}
class MathService {
@Memoize
fibonacci(n: number): number {
if (n <= 1) return n;
return this.fibonacci(n - 1) + this.fibonacci(n - 2);
}
}
const math = new MathService();
console.log(math.fibonacci(40)); // 很快,因为有缓存
带 TTL 过期策略版本:
interface CacheEntry<T> {
value: T;
expireAt: number;
}
function MemoizeWithTTL(ttlMs: number = 60_000) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
): PropertyDescriptor {
const originalMethod = descriptor.value;
const instanceCache = new WeakMap<
object,
Map<string, CacheEntry<unknown>>
>();
descriptor.value = function (this: object, ...args: unknown[]) {
if (!instanceCache.has(this)) {
instanceCache.set(this, new Map());
}
const cache = instanceCache.get(this)!;
const key = JSON.stringify(args);
const now = Date.now();
// 检查缓存是否存在且未过期
if (cache.has(key)) {
const entry = cache.get(key)!;
if (now < entry.expireAt) {
return entry.value;
}
// 过期则删除
cache.delete(key);
}
const result = originalMethod.apply(this, args);
cache.set(key, { value: result, expireAt: now + ttlMs });
return result;
};
return descriptor;
};
}
class DataService {
@MemoizeWithTTL(5000) // 缓存 5 秒
fetchConfig(key: string): string {
console.log('expensive computation for', key);
return `config_${key}_${Date.now()}`;
}
}
const svc = new DataService();
svc.fetchConfig('theme'); // expensive computation for theme
svc.fetchConfig('theme'); // 命中缓存,不输出
// 5 秒后...
svc.fetchConfig('theme'); // expensive computation for theme(缓存已过期)
TC39 标准装饰器版本:
function MemoizeNew<T extends (...args: any[]) => any>(
target: T,
context: ClassMethodDecoratorContext
): T {
const methodName = String(context.name);
const instanceCache = new WeakMap<object, Map<string, unknown>>();
function memoized(this: object, ...args: any[]): any {
if (!instanceCache.has(this)) {
instanceCache.set(this, new Map());
}
const cache = instanceCache.get(this)!;
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key);
}
const result = target.call(this, ...args);
cache.set(key, result);
return result;
}
return memoized as T;
}
class Calculator {
@MemoizeNew
expensiveCalc(n: number): number {
console.log('computing...');
return n * n;
}
}
支持异步方法的 Memoize:
function MemoizeAsync(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
): PropertyDescriptor {
const originalMethod = descriptor.value;
const instanceCache = new WeakMap<object, Map<string, Promise<unknown>>>();
descriptor.value = function (this: object, ...args: unknown[]) {
if (!instanceCache.has(this)) {
instanceCache.set(this, new Map());
}
const cache = instanceCache.get(this)!;
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key);
}
// 缓存 Promise 本身,实现请求去重
const promise = originalMethod.apply(this, args);
// 失败时清除缓存,允许重试
const cachedPromise = (promise as Promise<unknown>).catch(
(err: unknown) => {
cache.delete(key);
throw err;
}
);
cache.set(key, cachedPromise);
return cachedPromise;
};
return descriptor;
}
class ApiService {
@MemoizeAsync
async getUser(id: number): Promise<{ name: string }> {
console.log('fetching user', id);
const res = await fetch(`/api/users/${id}`);
return res.json();
}
}
const api = new ApiService();
// 同时发起两次相同请求,实际只发一次网络请求
const [user1, user2] = await Promise.all([
api.getUser(1),
api.getUser(1),
]);
要点
- 使用
WeakMap<object, Map>将缓存绑定到实例,不同实例不共享,实例销毁时缓存自动回收 JSON.stringify(args)做参数序列化,适用于基本类型参数;复杂对象需自定义 key 生成器- 异步版本缓存 Promise 本身,可实现请求去重(多次相同调用只触发一次实际请求)
- 失败时清除缓存,避免缓存错误结果