JavaScript中的装饰器模式
·
以下是关于 装饰器模式(Decorator Pattern) 的系统梳理,涵盖核心概念、实现方式、应用场景及注意事项,帮助我们掌握这种灵活扩展对象能力的经典设计模式:
一、装饰器模式基础
1. 核心概念
- 定义:动态地为对象添加额外职责,相比继承更灵活,遵循 开放-封闭原则(对扩展开放,对修改封闭)。
- 核心思想:通过包装对象(装饰器)增强功能,而非修改原始对象。
- 参与者:
- Component(组件接口):定义被装饰对象的接口。
- ConcreteComponent(具体组件):实现基础功能的对象。
- Decorator(装饰器基类):继承/实现组件接口,持有组件引用。
- ConcreteDecorator(具体装饰器):添加具体增强功能。
2. 基础实现(ES5 示例)
// 组件接口
class Coffee {
cost() { return 5; }
}
// 装饰器基类
class CoffeeDecorator {
constructor(coffee) {
this.coffee = coffee;
}
cost() {
return this.coffee.cost();
}
}
// 具体装饰器:加牛奶
class MilkDecorator extends CoffeeDecorator {
cost() {
return super.cost() + 2;
}
}
// 具体装饰器:加糖
class SugarDecorator extends CoffeeDecorator {
cost() {
return super.cost() + 1;
}
}
// 使用
let coffee = new Coffee();
coffee = new MilkDecorator(coffee);
coffee = new SugarDecorator(coffee);
console.log(coffee.cost()); // 8
二、JavaScript 装饰器语法(ES7+)
1. 类装饰器
function logClass(target) {
console.log(`装饰类: ${target.name}`);
return class extends target {
constructor(...args) {
super(...args);
console.log('实例化增强');
}
};
}
@logClass
class MyClass {
// ...
}
new MyClass(); // 输出:装饰类: MyClass → 实例化增强
2. 方法装饰器
function readonly(target, name, descriptor) {
descriptor.writable = false;
return descriptor;
}
class User {
@readonly
name = 'Alice';
}
const user = new User();
user.name = 'Bob'; // TypeError: Cannot assign to read only property
3. 属性装饰器
function formatDate(format) {
return function (target, key) {
let value = target[key];
Object.defineProperty(target, key, {
get: () => value.toLocaleDateString(format),
set: (v) => { value = new Date(v); },
});
};
}
class Event {
@formatDate('zh-CN')
date = new Date();
}
const event = new Event();
console.log(event.date); // "2023/10/20"
三、进阶知识
1. 装饰器组合
- 链式装饰:多个装饰器按声明顺序从外到内执行。
@decoratorA @decoratorB class MyClass { /* ... */ } // 等效于 decoratorA(decoratorB(MyClass))
2. 高阶装饰器(工厂模式)
- 参数化装饰器:通过函数返回装饰器实现动态配置。
function log(message) { return function (target, name, descriptor) { const original = descriptor.value; descriptor.value = function (...args) { console.log(`${message}: ${name}`); return original.apply(this, args); }; return descriptor; }; } class Calculator { @log('执行计算') add(a, b) { return a + b; } }
3. 装饰器与 Mixin
- 混合模式:通过装饰器实现多重继承。
function mixin(...mixins) { return target => Object.assign(target.prototype, ...mixins); } const CanSwim = { swim() { console.log('游泳'); } }; const CanFly = { fly() { console.log('飞行'); } }; @mixin(CanSwim, CanFly) class Duck {} const duck = new Duck(); duck.swim(); // 游泳
四、实际应用场景
1. 日志与性能监控
function benchmark(target, name, descriptor) {
const original = descriptor.value;
descriptor.value = function (...args) {
const start = performance.now();
const result = original.apply(this, args);
console.log(`${name}耗时: ${performance.now() - start}ms`);
return result;
};
return descriptor;
}
class DataProcessor {
@benchmark
process(data) { /* 耗时操作 */ }
}
2. 权限校验
function requireAuth(target, name, descriptor) {
const original = descriptor.value;
descriptor.value = function (...args) {
if (!this.isAuthenticated) {
throw new Error('需要登录');
}
return original.apply(this, args);
};
return descriptor;
}
class UserService {
isAuthenticated = true;
@requireAuth
deleteAccount() { /* ... */ }
}
3. 数据校验
function validate(schema) {
return function (target, name, descriptor) {
const original = descriptor.value;
descriptor.value = function (data) {
if (!schema.validate(data)) {
throw new Error('数据校验失败');
}
return original.call(this, data);
};
return descriptor;
};
}
class ApiService {
@validate({ name: 'string' })
createUser(data) { /* ... */ }
}
五、注意事项
1. 执行顺序陷阱
- 装饰器顺序影响结果:装饰器从外到内执行,但可能因嵌套导致逻辑混乱。
- 最佳实践:保持装饰器职责单一,避免相互依赖。
2. 原型链问题
- 方法装饰器修改原型:影响所有实例,需谨慎处理。
function decorator(target, name, descriptor) { // 直接修改原型方法 }
3. 浏览器兼容性
- ES7 装饰器语法:需通过 Babel(
@babel/plugin-proposal-decorators)转译。 - TypeScript 支持:需启用
experimentalDecorators编译器选项。
4. 过度使用风险
- 代码可读性下降:多层装饰器嵌套增加理解难度。
- 调试困难:装饰器可能隐藏原始逻辑,需结合 Source Map 调试。
六、装饰器模式 vs 代理模式
| 维度 | 装饰器模式 | 代理模式 |
|---|---|---|
| 核心目标 | 增强对象功能 | 控制对象访问(延迟初始化、权限) |
| 关系 | 装饰器与被装饰对象实现相同接口 | 代理与目标对象实现相同接口 |
| 关注点 | 动态添加职责 | 间接访问,功能隔离 |
七、总结与最佳实践
- 适用场景:动态扩展对象功能、AOP(日志/校验/监控)、兼容性封装。
- 优势:灵活扩展、符合单一职责原则、避免类爆炸。
- 替代方案:组合模式、策略模式(根据场景选择)。
- 现代应用:React 高阶组件(HOC)、Angular 装饰器语法、NestJS 依赖注入。
通过合理应用装饰器模式,可以优雅地扩展代码能力,但需注意保持简洁性和可维护性,避免陷入过度设计的陷阱。
更多推荐
所有评论(0)