从零开始理解 JavaScript 装饰器
装饰器(Decorator)是一种特殊的 JavaScript 语法,它允许你通过一种声明式的方式修改或增强类、类方法、类属性等的行为。就像它的名字"装饰"一样,它能为现有的代码添加额外的功能,而不需要直接修改原始代码。
1. 装饰器是什么?
想象你有一个礼物盒子(类或方法),装饰器就像是在这个盒子上系上丝带、贴上标签或加上其他装饰物,让盒子变得更漂亮或功能更多,但盒子本身并没有被改变。
2. 基本语法
装饰器使用 @ 符号开头,放在要装饰的目标前面:
@decorator
class MyClass {
@decorator
myMethod() {}
@decorator
myProperty = 'value';
}
3、装饰器的5种类型
3.1 类装饰器 - 装饰整个类
// 定义一个简单的类装饰器
function addAuthor(author) {
return function(target) { // 这是一个装饰器工厂
target.author = author; // 为类添加静态属性
return target;
}
}
@addAuthor('John Doe')
class Book {
// 类内容
}
console.log(Book.author); // 输出: "John Doe"
转译后的代码
// 原始的装饰器工厂函数保持不变
function addAuthor(author) {
return function(target) {
target.author = author; // 为类添加静态属性
return target;
};
}
// 定义Book类
class Book {
// 类内容
}
// 手动应用装饰器(替代 @addAuthor('John Doe') 语法)
const authorDecorator = addAuthor('John Doe');
const DecoratedBook = authorDecorator(Book);
// 测试
console.log(Book.author); // 输出: "John Doe"
console.log(DecoratedBook.author); // 输出: "John Doe"
3.2、方法装饰器 - 装饰类的方法
// 定义一个记录方法执行时间的装饰器
function logTime(target, name, descriptor) {
const originalMethod = descriptor.value;
descriptor.value = function(...args) {
console.time(name);
const result = originalMethod.apply(this, args);
console.timeEnd(name);
return result;
};
return descriptor;
}
class Calculator {
@logTime
add(a, b) {
return a + b;
}
}
const calc = new Calculator();
calc.add(2, 3); // 控制台会输出执行时间
相当于下述代码
// 原始的装饰器函数保持不变
function logTime(target, name, descriptor) {
const originalMethod = descriptor.value;
descriptor.value = function(...args) {
console.time(name);
const result = originalMethod.apply(this, args);
console.timeEnd(name);
return result;
};
return descriptor;
}
class Calculator {
add(a, b) {
return a + b;
}
}
// 手动应用装饰器(替代 @logTime 语法)
const addDescriptor = Object.getOwnPropertyDescriptor(Calculator.prototype, 'add');
const decoratedAddDescriptor = logTime(Calculator.prototype, 'add', addDescriptor);
Object.defineProperty(Calculator.prototype, 'add', decoratedAddDescriptor);
const calc = new Calculator();
calc.add(2, 3); // 控制台会输出执行时间
3.3、 属性装饰器 - 装饰类的属性
// 定义一个使属性只读的装饰器
function readonly(target, name, descriptor) {
descriptor.writable = false;
return descriptor;
}
class Person {
@readonly
name = 'Alice';
}
const person = new Person();
person.name = 'Bob'; // 这里会报错,因为name是只读的
3.4、访问器装饰器 - 装饰getter/setter
// 定义一个缓存结果的装饰器
function cache(target, name, descriptor) {
const getter = descriptor.get;
let cachedValue;
descriptor.get = function() {
if (cachedValue === undefined) {
cachedValue = getter.call(this);
}
return cachedValue;
};
return descriptor;
}
class ExpensiveComputation {
@cache
get result() {
console.log('计算中...');
return 42; // 假设这是个耗时的计算
}
}
const instance = new ExpensiveComputation();
console.log(instance.result); // 第一次会计算
console.log(instance.result); // 第二次直接从缓存读取
3.5、参数装饰器 - 装饰方法的参数
// 定义一个检查参数类型的装饰器
function checkType(type) {
return function(target, name, index) {
// 存储参数类型信息
target.__paramTypes = target.__paramTypes || {};
target.__paramTypes[name] = target.__paramTypes[name] || [];
target.__paramTypes[name][index] = type;
};
}
class MathOperations {
add(
@checkType('number') a,
@checkType('number') b
) {
return a + b;
}
}
// 实际类型检查需要在方法调用时实现
4、装饰器的执行顺序
装饰器的执行总是从上到下,从外到内的
- 参数装饰器)(先执行)
- 方法,属性,访问器装饰器
- 类装饰器(最后执行)
5、为什么要用装饰器?
- 代码更干净:将横切关注点(如日志,验证)与业务逻辑分开
- 复用性高:可以轻松在多个地方应用相同的功能
- 声明式编程:通过简单的注释就能添加复杂功能
- 可组合性:可以添加多个装饰器
6、实际应用场景
- 日志记录
- 性能监控
- 验证
- 依赖注入
- 权限控制
- 数据绑定