JavaScript装饰器模式详解与实战
装饰器模式是一种结构型设计模式,它允许你向一个现有的对象动态地添加新的功能,同时又不改变其结构。在JavaScript中,装饰器模式可以通过多种方式实现,包括传统的函数组合、高阶函数,以及ES7引入的装饰器语法。本文将深入探讨这些实现方式,并提供完整的代码示例。
装饰器模式的核心概念
装饰器模式的核心思想是“包装”。装饰器本身是一个函数或对象,它接收一个原始对象(或函数)作为参数,并返回一个增强后的版本。这种模式遵循开闭原则:对扩展开放,对修改关闭。你可以在不修改原始代码的情况下,通过装饰器为其添加日志、缓存、权限校验等横切关注点。
JavaScript中的传统实现方式
在ES6之前,JavaScript没有原生的装饰器语法,开发者通常使用高阶函数来模拟装饰器。下面是一个为函数添加执行时间统计的经典例子。
使用高阶函数装饰函数
高阶函数是指接收函数作为参数或返回函数的函数。我们可以创建一个装饰器工厂,它接收一个原始函数,返回一个带有附加功能的新函数。
// 原始函数:计算数组元素的和
function sum(arr) {
return arr.reduce((a, b) => a + b, 0);
}
// 装饰器函数:计算函数执行时间
function logExecutionTime(fn) {
return function(...args) {
console.time(fn.name + ' 执行时间');
const result = fn.apply(this, args);
console.timeEnd(fn.name + ' 执行时间');
return result;
};
}
// 使用装饰器
const decoratedSum = logExecutionTime(sum);
console.log(decoratedSum([1, 2, 3, 4, 5]));
// 控制台输出:sum 执行时间: 0.123ms (示例)
// 输出:15上述代码中,logExecutionTime是一个装饰器,它包装了原始sum函数,在执行前后添加了计时逻辑。这种方式灵活且无需修改原函数定义。
使用Proxy实现装饰器
ES6的Proxy对象可以拦截目标对象的操作,非常适合用来实现装饰器模式,尤其是当你需要装饰对象的方法或属性访问时。
class UserService {
getUser(id) {
// 模拟数据库查询
console.log(`查询用户ID: ${id}`);
return { id, name: '张三' };
}
}
// 装饰器:添加缓存功能
function withCache(target, key, descriptor) {
const cache = new Map();
const originalMethod = descriptor.value;
descriptor.value = function(...args) {
const cacheKey = JSON.stringify(args);
if (cache.has(cacheKey)) {
console.log('从缓存中返回');
return cache.get(cacheKey);
}
const result = originalMethod.apply(this, args);
cache.set(cacheKey, result);
return result;
};
return descriptor;
}
// 使用Proxy对实例进行装饰
const userService = new UserService();
const decoratedService = new Proxy(userService, {
get(target, prop) {
if (prop === 'getUser') {
const original = target[prop].bind(target);
return withCache(target, 'getUser', {
value: original,
writable: true,
configurable: true,
enumerable: true
}).value;
}
return target[prop];
}
});
console.log(decoratedService.getUser(1));
console.log(decoratedService.getUser(1)); // 第二次调用,从缓存返回虽然Proxy方式有些繁琐,但它能够拦截任意属性访问,非常适合在已有的类实例上动态添加行为,而不需要修改类本身。
ES7装饰器语法
ES7引入了装饰器提案,让装饰器模式在JavaScript中变得更加优雅。装饰器可以应用于类、方法、访问器、属性以及参数。目前该特性仍处于Stage 2阶段,使用时需要借助Babel或TypeScript进行转译。下面介绍最常见的类和方法装饰器。
安装与配置(使用Babel)
如果你希望在项目中使用装饰器语法,建议使用Babel的@babel/plugin-proposal-decorators插件。确保安装并配置好该插件。
npm install --save-dev @babel/plugin-proposal-decorators
然后在Babel配置文件(如babel.config.js)中添加插件:
module.exports = {
plugins: [
["@babel/plugin-proposal-decorators", { "legacy": true }]
]
};注意:legacy: true表示使用旧版(阶段1)的装饰器语法,这是目前最常用的兼容方式。
类装饰器
类装饰器是一个应用于类声明之前的函数,它接收一个参数:类的构造函数。装饰器可以修改类的行为,例如添加静态属性或修改原型。
// 定义一个类装饰器:给类添加一个静态属性 version
function addVersion(target) {
target.version = '1.0.0';
}
// 使用类装饰器
@addVersion
class MyService {
getName() {
return 'MyService';
}
}
console.log(MyService.version); // 输出: 1.0.0
const instance = new MyService();
console.log(instance.getName()); // 输出: MyService方法装饰器
方法装饰器应用于类的方法上,它接收三个参数:目标对象(如果是静态方法则为类本身)、方法名、以及属性描述符。方法装饰器常用于添加日志、性能监测、权限控制等。
// 方法装饰器:记录方法调用次数
function logCalls(target, name, descriptor) {
const original = descriptor.value;
const callCount = Symbol('callCount');
// 在目标对象上初始化计数属性
if (!target[callCount]) {
target[callCount] = 0;
}
descriptor.value = function(...args) {
target[callCount]++;
console.log(`方法 ${name} 被调用第 ${target[callCount]} 次`);
return original.apply(this, args);
};
return descriptor;
}
class UserManager {
@logCalls
createUser(name) {
console.log(`创建用户: ${name}`);
}
}
const manager = new UserManager();
manager.createUser('Alice');
manager.createUser('Bob');
// 控制台输出:
// 方法 createUser 被调用第 1 次
// 创建用户: Alice
// 方法 createUser 被调用第 2 次
// 创建用户: Bob访问器装饰器与属性装饰器
访问器装饰器(getter/setter)和方法装饰器类似,但接收的描述符是针对getter/setter的。属性装饰器在ES2018中有所限制,只能用于反射元数据,通常配合第三方库(如reflect-metadata)使用。
装饰器组合与参数化
多个装饰器可以叠加使用,执行顺序是从下往上(离类/方法最近的装饰器最先应用)。装饰器也可以接受参数,通过返回一个内部函数来实现。
// 带参数的装饰器:控制日志输出级别
function log(level = 'info') {
return function(target, name, descriptor) {
const original = descriptor.value;
descriptor.value = function(...args) {
console.log(`[${level}] 调用方法: ${name}`);
return original.apply(this, args);
};
return descriptor;
};
}
class DataParser {
@log('debug')
parseCSV(data) {
console.log('解析CSV数据...');
return data.split(',');
}
@log('warn')
validateData(data) {
console.log('验证数据...');
return true;
}
}
const parser = new DataParser();
parser.parseCSV('a,b,c');
parser.validateData({});装饰器模式的适用场景与注意事项
- 横切关注点:如日志、缓存、权限、事务管理。装饰器能将这些与核心业务逻辑分离。
- 不修改原始代码:当你无法访问或不想修改第三方库的代码时,装饰器提供了扩展手段。
- 保持单一职责:每个装饰器只负责一项附加功能,方便组合与复用。
- 注意性能:过度使用装饰器可能导致调用链过长,影响性能。在实际开发中应权衡使用。
- 语法支持:ES7装饰器目前仍是提案,生产环境建议使用转换工具(Babel/TypeScript)或采用传统高阶函数方式。
总结
装饰器模式在JavaScript中有多种实现形式,从经典的高阶函数、Proxy到ES7语法糖。它能够在不改变原有对象或函数的基础上,灵活地添加新功能,是编写可维护、可扩展代码的重要工具。建议开发者根据项目实际需求选择最合适的实现方式,在代码简洁性和浏览器兼容性之间取得平衡。