各位老铁,早上好!今天咱们聊点刺激的,不是相亲也不是理财,是JavaScript里的“装饰器”(Decorators)。这玩意儿,说白了,就是给你的代码“加Buff”,让它更强大、更灵活。别怕,听起来玄乎,其实上手贼简单。
一、啥是装饰器? 别跟我扯装修房子!
你可能听说过“装饰模式”,但那是一种设计模式。这里的装饰器,是JavaScript的一个提案(目前已经是Stage 3),它允许你以一种声明式的方式来修改或增强类、方法、属性,甚至参数的行为。
简单来说,装饰器就像一个函数,你可以把它“贴”在你的类、方法、属性前面,然后这个函数就会在运行时被调用,对你的代码进行一些“装饰”。 这种“装饰”可以是添加日志、权限验证、性能分析,或者任何你想做的事情。
二、语法结构: @
符号是关键!
JavaScript装饰器的语法非常简洁,使用 @
符号来表示。
@decorator
class MyClass {
@decorator
myMethod() {}
@decorator
myProperty = 123;
}
看到了吗? @decorator
就像一个标签,贴在了 MyClass
、myMethod
和 myProperty
前面。
三、装饰器类型: 针对不同目标,功能各异!
装饰器可以应用于不同的目标,根据目标的不同,装饰器的函数签名也不同。主要分为以下几类:
- 类装饰器 (Class Decorators): 装饰整个类。
- 方法装饰器 (Method Decorators): 装饰类的方法。
- 属性装饰器 (Property Decorators): 装饰类的属性。
- 访问器装饰器 (Accessor Decorators): 装饰类的 getter 和 setter。
- 参数装饰器 (Parameter Decorators): 装饰方法的参数。(这个用得比较少,先不展开)
四、类装饰器: 给类“穿马甲”!
类装饰器接收一个参数:类的构造函数。 你可以在装饰器里修改类的构造函数,添加新的属性或方法,甚至替换整个类。
function sealed(constructor: Function) {
Object.seal(constructor); // 冻结构造函数
Object.seal(constructor.prototype); // 冻结原型
}
@sealed
class BugReport {
constructor(public id: string) {}
submit() {
return "提交了bug报告";
}
}
const report = new BugReport("123");
console.log(report.submit()); // 输出: 提交了bug报告
// 尝试修改 BugReport 的原型,会报错 (严格模式下)
// BugReport.prototype.submit = function() { return "失败"; }; //TypeError: Cannot assign to read only property 'submit' of object '#<BugReport>'
在这个例子中,sealed
装饰器接收 BugReport
类的构造函数,然后使用 Object.seal
冻结了构造函数和原型。这样,就不能再修改这个类了,起到了保护的作用。
五、方法装饰器: 给方法“加特效”!
方法装饰器接收三个参数:
target
: 如果是静态方法,则是类的构造函数;如果是实例方法,则是类的原型对象。propertyKey
: 方法的名字,字符串类型。descriptor
: 属性描述符 (Property Descriptor),和Object.defineProperty()
里的描述符一样。
function logMethod(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`调用方法: ${propertyKey},参数: ${JSON.stringify(args)}`);
const result = originalMethod.apply(this, args); // 调用原始方法
console.log(`方法 ${propertyKey} 返回: ${result}`);
return result;
};
return descriptor; // 必须返回修改后的描述符
}
class Calculator {
@logMethod
add(x: number, y: number): number {
return x + y;
}
@logMethod
subtract(x: number, y: number): number {
return x - y;
}
}
const calculator = new Calculator();
calculator.add(5, 3); // 输出日志:调用方法: add,参数: [5,3] 方法 add 返回: 8
calculator.subtract(10, 4); // 输出日志:调用方法: subtract,参数: [10,4] 方法 subtract 返回: 6
在这个例子中,logMethod
装饰器给 add
和 subtract
方法添加了日志功能。每次调用这两个方法时,都会输出方法的名称、参数和返回值。
六、属性装饰器: 给属性“上保险”!
属性装饰器接收两个参数:
target
: 如果是静态属性,则是类的构造函数;如果是实例属性,则是类的原型对象。propertyKey
: 属性的名字,字符串类型。
需要注意的是,属性装饰器不能直接修改属性的值。 它的主要作用是给属性添加元数据,或者在属性被访问时执行一些操作。
import 'reflect-metadata'; // 引入 reflect-metadata,才能使用元数据
function readOnly(target: any, propertyKey: string) {
Reflect.defineMetadata('readonly', true, target, propertyKey);
Object.defineProperty(target, propertyKey, {
writable: false, // 设置为只读
});
}
class Person {
@readOnly
name: string = "张三";
constructor() {
//this.name = "李四"; // 报错:无法分配给只读属性 "name"
}
getName() {
console.log("getName", Reflect.getMetadata('readonly', Person.prototype, 'name'))
}
}
const person = new Person();
console.log(person.name); // 输出:张三
person.getName(); // 输出:true
//person.name = "李四"; // 报错:无法分配给只读属性 "name"
在这个例子中,readOnly
装饰器使用 Reflect.defineMetadata
给 name
属性添加了元数据,并且使用 Object.defineProperty
将属性设置为只读。
七、访问器装饰器: 给 getter/setter “加锁”!
访问器装饰器接收三个参数:
target
: 如果是静态访问器,则是类的构造函数;如果是实例访问器,则是类的原型对象。propertyKey
: 访问器的名字,字符串类型。descriptor
: 属性描述符 (Property Descriptor),和Object.defineProperty()
里的描述符一样。
function validate(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalSet = descriptor.set;
descriptor.set = function (value: number) {
if (value < 0) {
throw new Error(`Invalid value: ${value}. Value must be non-negative.`);
}
originalSet.call(this, value); // 调用原始的 setter
};
}
class Circle {
private _radius: number = 0;
@validate
set radius(value: number) {
this._radius = value;
}
get radius(): number {
return this._radius;
}
}
const circle = new Circle();
circle.radius = 5; // 正常
console.log(circle.radius); // 输出 5
//circle.radius = -2; // 报错:Invalid value: -2. Value must be non-negative.
在这个例子中,validate
装饰器给 radius
属性的 setter 添加了验证功能。如果设置的值小于 0,就会抛出错误。
八、装饰器工厂: 灵活定制,随心所欲!
有时候,我们需要根据不同的参数来定制装饰器的行为。 这时,可以使用装饰器工厂。
装饰器工厂就是一个返回装饰器函数的函数。 它可以接收一些参数,根据这些参数来生成不同的装饰器。
function logParameter(logMessage: string) {
return function (target: any, propertyKey: string, parameterIndex: number) {
console.log(`参数 ${parameterIndex} of ${propertyKey} will be logged with message: ${logMessage}`);
};
}
class Example {
greet(@logParameter("Name is being logged") name: string) {
console.log(`Hello, ${name}!`);
}
}
const example = new Example();
example.greet("小明"); // 输出:参数 0 of greet will be logged with message: Name is being logged Hello, 小明!
在这个例子中,logParameter
是一个装饰器工厂,它接收一个 logMessage
参数,并返回一个装饰器函数。 这个装饰器函数会输出参数的索引和消息。
九、元数据(Metadata): 装饰器的好帮手!
元数据是指描述数据的数据。 在装饰器中,我们可以使用元数据来存储一些关于类、方法或属性的信息。
要使用元数据,需要引入 reflect-metadata
库。
npm install reflect-metadata --save
然后在你的代码中导入:
import 'reflect-metadata';
reflect-metadata
提供了以下几个 API 来操作元数据:
Reflect.defineMetadata(key, value, target, propertyKey)
: 给目标对象添加元数据。Reflect.getMetadata(key, target, propertyKey)
: 获取目标对象的元数据。Reflect.hasMetadata(key, target, propertyKey)
: 检查目标对象是否包含指定的元数据。Reflect.deleteMetadata(key, target, propertyKey)
: 删除目标对象的元数据。
在上面的“属性装饰器”的例子中,我们已经演示了如何使用 Reflect.defineMetadata
来给属性添加元数据。
十、实际应用场景: 装饰器能干啥?
装饰器可以应用于很多场景,以下是一些常见的例子:
- 日志记录: 记录方法的调用和返回值。
- 权限验证: 验证用户是否有权限访问某个方法。
- 缓存: 缓存方法的计算结果,避免重复计算。
- 事务管理: 在方法执行前后开启和提交事务。
- 依赖注入: 将依赖项注入到类中。
- 状态管理: 将组件连接到状态管理系统(如 Redux)。
- 表单验证: 验证表单数据的有效性。
- AOP(面向切面编程): 将横切关注点(如日志、权限验证)从核心业务逻辑中分离出来。
十一、总结: 装饰器,让代码更优雅!
装饰器是一种强大的元编程工具,它可以让你以一种声明式的方式来修改或增强代码的行为。 它可以提高代码的可读性、可维护性和可重用性。
特性 | 描述 | 优点 |
---|---|---|
声明式编程 | 使用 @ 符号,以一种声明式的方式来应用装饰器。 |
代码更简洁、更易读,更易于理解代码的意图。 |
代码复用 | 可以将通用的逻辑(如日志、权限验证)封装成装饰器,然后在多个地方复用。 | 避免代码重复,提高代码的可维护性。 |
AOP | 可以将横切关注点(如日志、权限验证)从核心业务逻辑中分离出来。 | 使代码更模块化、更清晰,更容易进行单元测试。 |
元编程 | 可以在运行时修改或增强代码的行为。 | 可以实现一些高级的功能,如依赖注入、状态管理。 |
虽然装饰器目前还是一个提案,但已经被很多框架和库所采用,例如 Angular、NestJS 等。 相信在不久的将来,装饰器会成为 JavaScript 开发的标配。
十二、注意事项: 别玩脱了!
- TypeScript 支持更好: 装饰器最初是为 TypeScript 设计的,所以在 TypeScript 中使用装饰器体验更好。在 JavaScript 中使用装饰器需要 Babel 等工具的支持。
reflect-metadata
库: 如果需要使用元数据,需要引入reflect-metadata
库。- 执行顺序: 多个装饰器的执行顺序是从下往上,从左往右。
- 谨慎使用: 装饰器虽然强大,但也可能使代码变得复杂。 要谨慎使用,避免过度装饰。
- 浏览器兼容性: 装饰器目前还没有被所有浏览器原生支持,需要使用 Babel 等工具进行转译。
好了,今天就讲到这里。 希望大家能掌握装饰器的基本概念和用法,并在实际开发中灵活运用。 记住,代码要写得漂亮,更要写得实用! 散会!