各位观众,各位朋友,大家好!我是今天的主讲人,咱们今天来聊聊装饰器这个话题,特别是Stage 3阶段的Method、Field、Class装饰器,以及它们的执行顺序和Metadata反射。保证让大家听得懂,记得住,用得上!
装饰器:给你的代码穿上魔法外衣
首先,什么是装饰器?简单来说,装饰器就像给你的代码穿上了一件魔法外衣。这件外衣可以改变你代码的行为,增加新的功能,或者做一些其他神奇的事情,而不需要你直接修改源代码。
想象一下,你有一辆普通的汽车。你想要提升它的性能,但又不想拆掉发动机,重新造一辆车。这时,你可以给它加装一个涡轮增压器,或者换一套更好的悬挂系统。这些都是在不改变汽车原有结构的基础上,提升了汽车的性能。装饰器就是代码界的涡轮增压器和悬挂系统。
三种装饰器:Method, Field, Class
在Stage 3阶段的装饰器提案中,我们主要关注三种类型的装饰器:
- Method Decorator (方法装饰器):用来装饰类的方法,可以修改方法的行为,或者添加新的功能。
- Field Decorator (字段装饰器):用来装饰类的字段(属性),可以控制字段的访问和修改,或者添加一些额外的逻辑。
- Class Decorator (类装饰器):用来装饰整个类,可以修改类的定义,或者添加一些类的元数据。
接下来,我们分别详细介绍这三种装饰器。
Method Decorator:让你的方法更上一层楼
方法装饰器是最常用的装饰器之一。它可以用来修改方法的行为,或者添加一些新的功能。
基本语法:
function logMethod(target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
// target: 类的原型对象
// propertyKey: 方法名
// descriptor: 方法的属性描述符
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 MyClass {
@logMethod
add(a: number, b: number): number {
return a + b;
}
}
const myInstance = new MyClass();
myInstance.add(2, 3); // 输出日志信息,并返回 5
代码解释:
logMethod
是一个方法装饰器。它接收三个参数:target
: 类的原型对象 (MyClass.prototype
)。propertyKey
: 被装饰的方法名 ("add"
)。descriptor
: 方法的属性描述符。它包含方法的值、可写性、可枚举性和可配置性等信息。
- 在
logMethod
内部,我们首先保存了原始方法originalMethod
。 - 然后,我们修改了
descriptor.value
,使其指向一个新的函数。这个新函数会在调用原始方法之前和之后输出日志信息。 - 最后,我们返回修改后的
descriptor
。
应用场景:
- 日志记录:记录方法的调用和返回信息,方便调试和分析。
- 性能监控:测量方法的执行时间,找出性能瓶颈。
- 权限控制:检查用户是否有权限调用某个方法。
- 缓存:缓存方法的返回值,避免重复计算。
Field Decorator:保护你的字段,添加约束
字段装饰器可以用来装饰类的字段(属性)。它可以控制字段的访问和修改,或者添加一些额外的逻辑。
基本语法:
function readonly(target: Object, propertyKey: string) {
// target: 类的原型对象
// propertyKey: 字段名
let _val: any = target[propertyKey]; // 保存字段的原始值
Object.defineProperty(target, propertyKey, {
get: function() {
return _val;
},
set: function(newVal) {
console.log(`Attempting to set readonly property ${propertyKey} to ${newVal}`);
// 不允许修改
}
});
}
class MyClass {
@readonly
name: string = "John Doe";
constructor() {
// this.name = "Jane Doe"; // 这行代码不会生效,因为 name 属性是只读的
}
}
const myInstance = new MyClass();
console.log(myInstance.name); // 输出 "John Doe"
myInstance.name = "Jane Doe"; // 输出日志信息,但 name 属性的值不会改变
console.log(myInstance.name); // 输出 "John Doe"
代码解释:
readonly
是一个字段装饰器。它接收两个参数:target
: 类的原型对象 (MyClass.prototype
)。propertyKey
: 被装饰的字段名 ("name"
)。
- 在
readonly
内部,我们首先保存了字段的原始值。 - 然后,我们使用
Object.defineProperty
来重新定义字段的属性描述符。我们修改了get
和set
方法,使其成为只读属性。 get
方法返回字段的原始值。set
方法输出一条日志信息,但不允许修改字段的值。
应用场景:
- 只读属性:防止字段被意外修改。
- 数据验证:在设置字段的值之前进行验证,确保数据的有效性。
- 计算属性:根据其他字段的值动态计算出当前字段的值。
- 延迟加载:在第一次访问字段时才进行初始化。
Class Decorator:改变类的命运
类装饰器可以用来装饰整个类。它可以修改类的定义,或者添加一些类的元数据。
基本语法:
function sealed(constructor: Function) {
// constructor: 类的构造函数
Object.seal(constructor); // 密封构造函数
Object.seal(constructor.prototype); // 密封原型对象
}
@sealed
class MyClass {
name: string = "John Doe";
}
// MyClass.prototype.age = 30; // 这行代码会报错,因为原型对象已经被密封了
const myInstance = new MyClass();
myInstance.name = "Jane Doe"; // 仍然可以修改实例属性
代码解释:
sealed
是一个类装饰器。它接收一个参数:constructor
: 类的构造函数。
- 在
sealed
内部,我们使用Object.seal
来密封构造函数和原型对象。这可以防止向类添加新的属性或方法。
应用场景:
- 密封类:防止类被修改,提高代码的安全性。
- 注册类:将类注册到某个系统中,方便管理和使用。
- 依赖注入:将类的依赖项注入到类的构造函数中。
- 元数据管理:添加类的元数据,方便运行时获取类的相关信息。
装饰器的执行顺序:先内后外,先下后上
当一个目标(方法、字段或类)有多个装饰器时,它们的执行顺序是非常重要的。记住一个原则:先内后外,先下后上。
- 先内后外:对于嵌套的装饰器,先执行最内层的装饰器,再执行外层的装饰器。
- 先下后上:对于同级别的装饰器,先执行下面的装饰器,再执行上面的装饰器。
举例说明:
function first(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log("first(): evaluated");
return descriptor;
}
function second(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log("second(): evaluated");
return descriptor;
}
class ExampleClass {
@first
@second
method() {
console.log("method(): called");
}
}
const example = new ExampleClass();
example.method();
执行结果:
second(): evaluated
first(): evaluated
method(): called
解释:
- 首先,
@second
装饰器被执行。 - 然后,
@first
装饰器被执行。 - 最后,
method
方法被调用。
之所以是这个顺序,可以这么理解:@second
离 method
更近,所以先执行,它相当于给 method
穿上了第一层外衣。@first
在 second
的外面,相当于给已经穿了外衣的 method
再穿上一层外衣。所以先执行 second
,再执行 first
。
Metadata 反射:让你的代码拥有自知之明
Metadata 反射是一种在运行时获取类型信息的技术。它可以让你在不知道具体类型的情况下,获取类的属性、方法、参数等信息。
基本用法:
首先,你需要安装 reflect-metadata
包:
npm install reflect-metadata --save
然后在你的 TypeScript 代码中导入 reflect-metadata
:
import "reflect-metadata";
示例:
import "reflect-metadata";
function logType(target: any, propertyKey: string) {
const type = Reflect.getMetadata("design:type", target, propertyKey);
console.log(`${propertyKey} type: ${type.name}`);
}
class MyClass {
@logType
name: string;
@logType
age: number;
}
执行结果:
name type: String
age type: Number
代码解释:
logType
是一个字段装饰器。Reflect.getMetadata("design:type", target, propertyKey)
可以获取字段的类型信息。type.name
可以获取类型的名称。
常用的 Metadata Key:
Metadata Key | 描述 |
---|---|
design:type |
字段或方法的类型 |
design:paramtypes |
方法的参数类型 |
design:returntype |
方法的返回值类型 |
应用场景:
- 依赖注入:根据参数类型自动注入依赖项。
- 序列化/反序列化:根据字段类型自动进行序列化和反序列化。
- ORM:根据字段类型自动映射到数据库表字段。
- AOP:根据方法参数和返回值类型进行切面编程。
总结:装饰器和 Metadata 反射的强大组合
装饰器和 Metadata 反射是一对强大的组合。它们可以让你在不修改源代码的情况下,改变代码的行为,添加新的功能,或者获取代码的元数据。
装饰器的优势:
- 可读性好:装饰器语法简洁明了,易于理解。
- 可维护性高:装饰器可以独立于业务逻辑进行修改和维护。
- 可重用性强:装饰器可以应用到多个目标上,提高代码的重用性。
Metadata 反射的优势:
- 运行时类型信息:可以在运行时获取类型信息,方便进行动态处理。
- 解耦:可以将类型信息和业务逻辑解耦,提高代码的灵活性。
- 扩展性强:可以自定义 Metadata Key,扩展 Metadata 反射的功能。
示例:使用装饰器和 Metadata 反射实现简单的依赖注入
import "reflect-metadata";
const Injectable = (target: any) => {
Reflect.defineMetadata('injectable', true, target);
return target;
};
const Inject = (target: any, propertyKey: string | symbol, index: number) => {
const type = Reflect.getMetadata('design:paramtypes', target, propertyKey)[index];
Reflect.defineMetadata('inject', type, target, propertyKey);
};
const resolve = <T>(target: any): T => {
const injectable = Reflect.getMetadata('injectable', target);
if (!injectable) {
throw new Error('Target is not injectable');
}
const types = Reflect.getMetadata('design:paramtypes', target) || [];
const dependencies = types.map((type: any) => resolve(type));
return new target(...dependencies);
};
@Injectable
class Logger {
log(message: string) {
console.log(`Logger: ${message}`);
}
}
@Injectable
class UserService {
constructor(@Inject private readonly logger: Logger) {}
createUser(name: string) {
this.logger.log(`Creating user: ${name}`);
}
}
const userService = resolve<UserService>(UserService);
userService.createUser('Alice'); // Output: Logger: Creating user: Alice
在这个例子中,@Injectable
装饰器标记一个类可以被依赖注入。@Inject
装饰器标记构造函数参数需要被注入的依赖。resolve
函数负责解析依赖关系并创建实例。
总结
今天我们深入探讨了Stage 3的装饰器,包括方法装饰器、字段装饰器和类装饰器,以及它们的执行顺序和Metadata反射。希望通过今天的讲解,大家能够更好地理解和应用装饰器,编写出更优雅、更灵活的代码。装饰器就像是给你的代码进行了一次华丽的升级,让你的代码更上一层楼。感谢大家的收听,我们下次再见!