说说你对 TypeScript 中装饰器的理解?应用场景?
问题解析
装饰器是 TypeScript 的实验性特性,考察候选人对装饰器概念、分类、执行顺序以及实际应用(如 Angular、NestJS、类库开发)的理解。
核心概念
- 装饰器(Decorator):一种特殊类型的声明,可以附加到类声明、方法、访问符、属性或参数上
- 装饰器工厂:返回装饰器函数的函数,可接收参数
- 装饰器组合:多个装饰器的执行顺序
- 元数据反射:
reflect-metadata库实现元数据存储
详细解答
1. 是什么
装饰器是一种特殊类型的声明,它能够被附加到类声明、方法、访问符、属性或参数上。
是一种在不改变原类和使用继承的情况下,动态地扩展对象功能。
同样的,本质也不是什么高大上的结构,就是一个普通的函数,@expression 的形式其实是 Object.defineProperty 的语法糖。
expression 求值后必须也是一个函数,它会在运行时被调用,被装饰的声明信息作为参数传入。
2. 启用装饰器
由于装饰器是一个实验性特性,若要使用,需要在 tsconfig.json 文件中启用:
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true,
"emitDecoratorMetadata": true // 如果需要元数据支持
}
}
3. 装饰器的分类
3.1 类装饰器
类装饰器应用于类构造函数,可以用来监视、修改或替换类定义。
function addAge(constructor: Function) {
constructor.prototype.age = 18;
}
@addAge
class Person {
name: string;
age!: number; // 非空断言
constructor() {
this.name = 'huihui';
}
}
let person = new Person();
console.log(person.age); // 18
原理:上述代码实际等同于 Person = addAge(function Person() { ... })。
3.2 方法/属性装饰器
方法装饰器接收 3 个参数:
target:对象的原型propertyKey:方法的名称descriptor:方法的属性描述符
function method(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log(target); // 原型对象
console.log("prop " + propertyKey); // 方法名
console.log("desc " + JSON.stringify(descriptor));
descriptor.writable = false; // 设置为不可写
}
function property(target: any, propertyKey: string) {
console.log("target", target);
console.log("propertyKey", propertyKey);
}
class Person {
@property
name: string;
constructor() {
this.name = 'huihui';
}
@method
say() {
return 'instance method';
}
@method
static run() {
return 'static method';
}
}
const xmz = new Person();
// xmz.say = function() { return 'edit' }; // Error: 只读属性
3.3 参数装饰器
参数装饰器接收 3 个参数:
target:当前对象的原型propertyKey:参数的名称index:参数数组中的位置
function logParameter(target: Object, propertyName: string, index: number) {
console.log(target); // 原型对象
console.log(propertyName); // 方法名
console.log(index); // 参数索引
}
class Employee {
greet(@logParameter message: string): string {
return `hello ${message}`;
}
}
const emp = new Employee();
emp.greet('hello');
3.4 访问器装饰器
访问器装饰器使用起来方式与方法装饰一致:
function modification(target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
console.log(target);
console.log("prop " + propertyKey);
console.log("desc " + JSON.stringify(descriptor));
}
class Person {
private _name: string;
constructor() {
this._name = 'huihui';
}
@modification
get name() {
return this._name;
}
}
4. 装饰器工厂
如果想要传递参数,使装饰器变成类似工厂函数,只需要在装饰器函数内部再返回一个函数:
function addAge(age: number) {
return function(constructor: Function) {
constructor.prototype.age = age;
};
}
@addAge(10)
class Person {
name: string;
age!: number;
constructor() {
this.name = 'huihui';
}
}
let person = new Person();
console.log(person.age); // 10
5. 装饰器执行顺序
当多个装饰器应用于一个声明上,由上至下依次对装饰器表达式求值,求值的结果会被当作函数,由下至上依次调用。
function f() {
console.log("f(): evaluated");
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log("f(): called");
};
}
function g() {
console.log("g(): evaluated");
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log("g(): called");
};
}
class C {
@f()
@g()
method() {}
}
// 输出:
// f(): evaluated
// g(): evaluated
// g(): called
// f(): called
深入理解
元数据反射
使用 reflect-metadata 库可以实现元数据的存储和读取:
import 'reflect-metadata';
const requiredMetadataKey = Symbol('required');
function required(target: Object, propertyKey: string | symbol, parameterIndex: number) {
const existingRequiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyKey) || [];
existingRequiredParameters.push(parameterIndex);
Reflect.defineMetadata(requiredMetadataKey, existingRequiredParameters, target, propertyKey);
}
function validate(target: any, propertyName: string, descriptor: TypedPropertyDescriptor<Function>) {
const method = descriptor.value!;
descriptor.value = function (...args: any[]) {
const requiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyName);
if (requiredParameters) {
for (const parameterIndex of requiredParameters) {
if (parameterIndex >= args.length || args[parameterIndex] === undefined) {
throw new Error('Missing required argument.');
}
}
}
return method.apply(this, args);
};
}
class Greeter {
greeting: string;
constructor(greeting: string) {
this.greeting = greeting;
}
@validate
greet(@required name: string) {
return `Hello ${name}, ${this.greeting}`;
}
}
应用场景
1. 日志记录
function log(target: any, key: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Calling ${key} with`, args);
const result = originalMethod.apply(this, args);
console.log(`Result:`, result);
return result;
};
return descriptor;
}
class Calculator {
@log
add(a: number, b: number) {
return a + b;
}
}
2. 权限控制
function requireAuth(roles: string[]) {
return function(target: any, key: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
const currentUser = getCurrentUser();
if (!currentUser || !roles.includes(currentUser.role)) {
throw new Error('Unauthorized');
}
return originalMethod.apply(this, args);
};
};
}
class AdminController {
@requireAuth(['admin', 'superadmin'])
deleteUser(id: string) {
// 删除用户逻辑
}
}
3. 防抖/节流
function debounce(delay: number) {
return function(target: any, key: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
let timer: NodeJS.Timeout;
descriptor.value = function (...args: any[]) {
clearTimeout(timer);
timer = setTimeout(() => {
originalMethod.apply(this, args);
}, delay);
};
};
}
class SearchComponent {
@debounce(300)
onSearch(keyword: string) {
// 搜索逻辑
}
}
4. Angular 框架
Angular 大量使用装饰器:
@Component({
selector: 'app-hero',
template: '<div>{{name}}</div>'
})
class HeroComponent {
@Input() name: string;
@Output() onSelect = new EventEmitter();
constructor(private service: HeroService) {}
@HostListener('click')
handleClick() {
this.onSelect.emit(this.name);
}
}
最佳实践
- 谨慎使用:装饰器仍是实验性特性,API 可能变化
- 单一职责:每个装饰器只做一件事
- 类型安全:为装饰器参数和返回值添加类型
- 文档说明:装饰器会改变代码行为,需要清晰的文档
- 测试覆盖:装饰器逻辑需要充分的单元测试
面试要点
- 装饰器本质:普通函数,
@expression是Object.defineProperty的语法糖 - 启用方式:
tsconfig.json中设置experimentalDecorators: true - 分类:类装饰器、方法/属性装饰器、参数装饰器、访问器装饰器
- 装饰器工厂:返回装饰器函数的函数,支持传参
- 执行顺序:由上至下求值,由下至上调用
- 应用场景:
- 日志记录
- 权限控制
- 防抖节流
- 框架开发(Angular、NestJS)
- 元数据反射:使用
reflect-metadata存储和读取元数据