各位靓仔靓女,今天咱们聊点新鲜玩意儿,Decorator!别害怕,不是装修工,是JavaScript里的“装饰器”,但它在TypeScript里玩得更溜。今天咱们就来扒一扒它的底裤,看看它到底是个什么东西,怎么用,以及为什么要用它。
开场白:装饰器是个啥?
想象一下,你有一个普通的蛋糕,你想让它更吸引人,更好吃。你可以加点奶油,放点水果,撒点巧克力粉。这些“加料”的过程,就是装饰。在编程世界里,装饰器就是用来给你的类、方法、属性或者参数“加料”的。它可以扩展功能,修改行为,而不用修改原有的代码。
JavaScript的Decorator:犹抱琵琶半遮面
在原生的JavaScript里,Decorator还是个实验性的特性,需要通过Babel之类的工具转换才能使用。所以,咱们今天主要聚焦在TypeScript里,因为TypeScript对Decorator的支持更好,更稳定。
TypeScript的Decorator:闪亮登场
TypeScript的Decorator是一种特殊的声明,它可以被附加到类声明、方法、访问符、属性或参数上。它们使用@expression
这种形式,其中expression
必须是一个会返回函数的表达式,这个函数会在运行时被调用,并传入被装饰的对象的信息。
Decorator的种类:各司其职
Decorator主要分为四种:
- 类装饰器 (Class Decorators)
- 方法装饰器 (Method Decorators)
- 访问器装饰器 (Accessor Decorators)
- 属性装饰器 (Property Decorators)
- 参数装饰器 (Parameter Decorators)
咱们一个一个来看。
1. 类装饰器 (Class Decorators):给类穿新衣
类装饰器用来装饰整个类。它接收一个参数,就是被装饰的类本身。你可以用它来修改类的行为,添加新的属性或方法,甚至替换整个类。
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
@sealed
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
// 使用了 @sealed 装饰器,Greeter 类和它的原型对象都被冻结了,无法修改
上面的例子中,@sealed
就是一个类装饰器。它接收 Greeter
类的构造函数作为参数,并使用 Object.seal
方法冻结了构造函数和它的原型对象。这样,就防止了在运行时对 Greeter
类进行修改。
再来一个更实际的例子:记录类的创建次数
function logClassCreation(constructor: Function) {
let creationCount = 0;
return class extends (constructor as { new (...args: any[]): any }) {
constructor(...args: any[]) {
super(...args);
creationCount++;
console.log(`Class ${constructor.name} created ${creationCount} times.`);
}
};
}
@logClassCreation
class MyService {
constructor() {
console.log("MyService constructor called");
}
}
const service1 = new MyService(); // 输出:Class MyService created 1 times. MyService constructor called
const service2 = new MyService(); // 输出:Class MyService created 2 times. MyService constructor called
这个例子中,logClassCreation
装饰器会记录 MyService
类被创建的次数,并在每次创建时输出日志。 注意这里使用了构造函数签名 (constructor as { new (...args: any[]): any })
, 这是告诉 TypeScript 编译器,我们传入的 constructor
是一个可以被 new
调用的构造函数。
2. 方法装饰器 (Method Decorators):给方法加Buff
方法装饰器用来装饰类的方法。它接收三个参数:
target
: 如果是静态成员,则是类的构造函数;如果是实例成员,则是类的原型对象。propertyKey
: 方法的名字。descriptor
: 方法的属性描述符。
方法装饰器可以用来修改方法的行为,比如添加日志、验证参数、缓存结果等等。
function logMethod(target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Calling method ${propertyKey} with arguments: ${JSON.stringify(args)}`);
const result = originalMethod.apply(this, args);
console.log(`Method ${propertyKey} returned: ${result}`);
return result;
};
return descriptor;
}
class Calculator {
@logMethod
add(x: number, y: number): number {
return x + y;
}
}
const calculator = new Calculator();
calculator.add(2, 3); // 输出:Calling method add with arguments: [2,3] Method add returned: 5
这个例子中,@logMethod
装饰器会记录 add
方法的调用信息,包括参数和返回值。
再来一个:防止方法被过快连续调用(防抖)
function debounce(delay: number) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
let timeoutId: number;
descriptor.value = function (...args: any[]) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
originalMethod.apply(this, args);
}, delay);
};
return descriptor;
};
}
class MyComponent {
@debounce(300)
onInputChange(event: any) {
console.log('Input changed:', event.target.value);
}
}
const component = new MyComponent();
const inputElement = { target: { value: 'a' } };
component.onInputChange(inputElement);
component.onInputChange(inputElement);
component.onInputChange(inputElement); // 只有最后一次会执行,延迟 300ms
这个例子中,@debounce
装饰器会防止 onInputChange
方法被过快连续调用,只有在延迟 300ms
后才会真正执行。
3. 访问器装饰器 (Accessor Decorators):控制属性的读写
访问器装饰器用来装饰类的 getter 或 setter。它接收三个参数:
target
: 如果是静态成员,则是类的构造函数;如果是实例成员,则是类的原型对象。propertyKey
: 访问器的名字。descriptor
: 访问器的属性描述符。
访问器装饰器可以用来控制属性的读写权限,添加验证逻辑等等。
function validateAge(target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
const originalSet = descriptor.set;
descriptor.set = function (value: number) {
if (value < 0 || value > 150) {
throw new Error("Invalid age");
}
originalSet.call(this, value);
};
return descriptor;
}
class Person {
private _age: number;
@validateAge
set age(value: number) {
this._age = value;
}
get age() {
return this._age;
}
}
const person = new Person();
person.age = 30; // 正常赋值
// person.age = -10; // 抛出错误:Invalid age
这个例子中,@validateAge
装饰器会验证 age
属性的 setter 方法,只有当年龄在 0 到 150 之间时才能赋值。
4. 属性装饰器 (Property Decorators):给属性加点料
属性装饰器用来装饰类的属性。它接收两个参数:
target
: 如果是静态成员,则是类的构造函数;如果是实例成员,则是类的原型对象。propertyKey
: 属性的名字。
属性装饰器可以用来修改属性的元数据,添加默认值等等。
import 'reflect-metadata';
const requiredMetadataKey = Symbol("required");
function required(target: Object, propertyKey: string | symbol, parameterIndex: number) {
let 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>) {
let method = descriptor.value!;
descriptor.value = function () {
let requiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyName);
if (requiredParameters) {
for (let parameterIndex of requiredParameters) {
if (arguments.length <= parameterIndex || arguments[parameterIndex] === undefined) {
throw new Error("Missing required argument.");
}
}
}
return method.apply(this, arguments);
}
}
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
@validate
greet(@required name: string) {
return "Hello " + name + ", " + this.greeting;
}
}
5. 参数装饰器 (Parameter Decorators):验证参数
参数装饰器用于装饰函数的参数。它接收三个参数:
target
: 如果是静态成员,则是类的构造函数;如果是实例成员,则是类的原型对象。propertyKey
: 方法的名字。parameterIndex
: 参数在参数列表中的索引。
参数装饰器通常与方法装饰器一起使用,用来验证参数的有效性。
import 'reflect-metadata';
const requiredMetadataKey = Symbol("required");
function required(target: Object, propertyKey: string | symbol, parameterIndex: number) {
let 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>) {
let method = descriptor.value!;
descriptor.value = function () {
let requiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyName);
if (requiredParameters) {
for (let parameterIndex of requiredParameters) {
if (arguments.length <= parameterIndex || arguments[parameterIndex] === undefined) {
throw new Error("Missing required argument.");
}
}
}
return method.apply(this, arguments);
}
}
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
@validate
greet(@required name: string) {
return "Hello " + name + ", " + this.greeting;
}
}
const greeter = new Greeter("World");
// greeter.greet(); // 报错:Missing required argument.
greeter.greet("TypeScript"); // 输出:Hello TypeScript, World
这个例子中,@required
装饰器标记了 greet
方法的 name
参数是必须的。 validate
方法装饰器验证参数。
Decorator的应用场景:大显身手
Decorator在实际开发中有很多应用场景,比如:
- 日志记录: 记录方法的调用信息,方便调试和监控。
- 权限验证: 验证用户是否有权限访问某个方法。
- 缓存: 缓存方法的返回值,提高性能。
- 依赖注入: 将依赖注入到类中,降低耦合度。
- AOP(面向切面编程): 将一些通用的逻辑(比如日志、权限验证)从业务逻辑中分离出来,提高代码的可维护性。
- 数据校验: 验证数据的有效性。
- 路由管理: 在框架中,可以使用装饰器来定义路由。例如,NestJS框架就大量使用了Decorator。
Decorator的优缺点:爱恨交织
优点:
- 代码复用: 可以将一些通用的逻辑提取出来,在多个地方复用。
- 可读性: 使用装饰器可以使代码更简洁,更易于阅读。
- 可维护性: 装饰器可以将一些横切关注点(比如日志、权限验证)从业务逻辑中分离出来,提高代码的可维护性。
- 扩展性: 使用装饰器可以方便地扩展类的功能,而不需要修改原有的代码。
缺点:
- 学习成本: 需要学习Decorator的语法和原理。
- 调试难度: Decorator的执行顺序可能会比较复杂,调试起来比较困难。
- 元数据: 使用
reflect-metadata
会增加代码的体积。
注意事项:
- 开启实验性支持: 需要在
tsconfig.json
文件中开启experimentalDecorators
选项。 - 理解Decorator的执行顺序: Decorator的执行顺序是从下到上,从右到左。
- 谨慎使用
reflect-metadata
: 如果不需要使用元数据,尽量避免使用reflect-metadata
,以减小代码的体积。
总结:善用利器,事半功倍
Decorator是一种强大的编程技术,它可以提高代码的复用性、可读性和可维护性。但是,Decorator也有一些缺点,需要谨慎使用。在实际开发中,应该根据具体的场景选择是否使用Decorator。 希望今天的讲解对大家有所帮助! 咱们下次再见!