您现在的位置是:网站首页 > 装饰器模式(Decorator)的ES7实现文章详情
装饰器模式(Decorator)的ES7实现
陈川
【
JavaScript
】
31725人已围观
10196字
装饰器模式是一种结构型设计模式,允许在不改变对象自身的基础上动态地添加功能。ES7引入了装饰器语法,使得在JavaScript中实现装饰器模式更加直观和优雅。通过装饰器,可以轻松地扩展类、方法或属性的行为,而无需修改原始代码。
装饰器模式的核心概念
装饰器模式的核心在于"包装":通过将一个对象放入另一个对象(装饰器)中,动态地添加职责。装饰器与被装饰对象实现相同的接口,因此对客户端代码透明。在ES7中,装饰器通过@
符号实现,本质上是一个高阶函数,接收目标对象并返回修改后的版本。
装饰器的典型应用场景包括:
- 日志记录
- 权限控制
- 性能监控
- 数据验证
- 缓存处理
ES7装饰器基础语法
ES7装饰器目前是Stage 3提案,需要通过Babel等工具转换才能使用。装饰器可以应用于:
- 类装饰器
- 类方法装饰器
- 类属性装饰器
- 访问器装饰器
基本语法结构如下:
@decorator
class MyClass {}
class MyClass {
@decorator
method() {}
@decorator
property = 'value';
@decorator
get accessor() {}
}
类装饰器实现
类装饰器接收构造函数作为参数,可以返回新的构造函数来替换原类。下面是一个添加混入功能的类装饰器示例:
function mixin(...mixins) {
return function(target) {
Object.assign(target.prototype, ...mixins);
};
}
const LoggerMixin = {
log(message) {
console.log(`[${this.constructor.name}] ${message}`);
}
};
const SerializableMixin = {
serialize() {
return JSON.stringify(this);
}
};
@mixin(LoggerMixin, SerializableMixin)
class User {
constructor(name, age) {
this.name = name;
this.age = age;
}
}
const user = new User('Alice', 30);
user.log('Hello'); // "[User] Hello"
console.log(user.serialize()); // "{"name":"Alice","age":30}"
方法装饰器详解
方法装饰器接收三个参数:
- 目标对象(类原型)
- 方法名
- 属性描述符
下面是一个测量方法执行时间的方法装饰器:
function measureTime(target, name, descriptor) {
const originalMethod = descriptor.value;
descriptor.value = function(...args) {
const start = performance.now();
const result = originalMethod.apply(this, args);
const end = performance.now();
console.log(`Method ${name} took ${end - start}ms`);
return result;
};
return descriptor;
}
class Calculator {
@measureTime
heavyCalculation() {
let result = 0;
for (let i = 0; i < 100000000; i++) {
result += Math.sqrt(i);
}
return result;
}
}
const calc = new Calculator();
calc.heavyCalculation(); // 控制台输出执行时间
属性装饰器应用
属性装饰器接收两个参数:
- 目标对象
- 属性名
下面是一个实现属性观察者的装饰器:
function observable(target, key) {
let value = target[key];
Object.defineProperty(target, key, {
get() {
return value;
},
set(newValue) {
console.log(`Setting ${key} from ${value} to ${newValue}`);
value = newValue;
}
});
}
class Person {
@observable
name = 'Anonymous';
@observable
age = 0;
}
const person = new Person();
person.name = 'Bob'; // 控制台输出: "Setting name from Anonymous to Bob"
person.age = 25; // 控制台输出: "Setting age from 0 to 25"
访问器装饰器示例
访问器装饰器与方法装饰器类似,可以装饰getter和setter。下面是一个实现只读属性的装饰器:
function readonly(target, name, descriptor) {
descriptor.writable = false;
return descriptor;
}
class Circle {
constructor(radius) {
this._radius = radius;
}
@readonly
get diameter() {
return this._radius * 2;
}
}
const circle = new Circle(5);
console.log(circle.diameter); // 10
circle.diameter = 20; // TypeError: Cannot assign to read only property
装饰器工厂模式
装饰器工厂是返回装饰器函数的函数,允许通过参数配置装饰器行为。下面是一个可配置的缓存装饰器:
function cache(times) {
return function(target, name, descriptor) {
const originalMethod = descriptor.value;
let cacheMap = new Map();
let callCount = 0;
descriptor.value = function(...args) {
const key = JSON.stringify(args);
if (cacheMap.has(key)) {
return cacheMap.get(key);
}
const result = originalMethod.apply(this, args);
cacheMap.set(key, result);
if (++callCount > times) {
cacheMap.delete(cacheMap.keys().next().value);
callCount--;
}
return result;
};
return descriptor;
};
}
class MathOperations {
@cache(3)
factorial(n) {
console.log('Calculating factorial for', n);
return n <= 1 ? 1 : n * this.factorial(n - 1);
}
}
const math = new MathOperations();
console.log(math.factorial(5)); // 计算并缓存
console.log(math.factorial(5)); // 从缓存读取
多个装饰器的执行顺序
当多个装饰器应用于同一个目标时,执行顺序遵循特定规则:
- 装饰器工厂先从上到下执行
- 装饰器函数再从下到上执行
function decoratorA() {
console.log('decoratorA factory');
return function(target, name, descriptor) {
console.log('decoratorA applied');
};
}
function decoratorB() {
console.log('decoratorB factory');
return function(target, name, descriptor) {
console.log('decoratorB applied');
};
}
class Example {
@decoratorA()
@decoratorB()
method() {}
}
// 控制台输出:
// decoratorA factory
// decoratorB factory
// decoratorB applied
// decoratorA applied
实际应用:API请求装饰器
结合装饰器模式与async/await,可以创建强大的API请求处理装饰器:
function apiEndpoint(method, path) {
return function(target, name, descriptor) {
const originalMethod = descriptor.value;
descriptor.value = async function(...args) {
try {
console.log(`Calling ${method} ${path}`);
const response = await fetch(path, {
method,
headers: {
'Content-Type': 'application/json'
},
body: method !== 'GET' ? JSON.stringify(args[0]) : undefined
});
if (!response.ok) {
throw new Error(`API request failed: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error(`API call ${name} failed:`, error);
throw error;
}
};
return descriptor;
};
}
class UserService {
@apiEndpoint('GET', '/api/users')
static async getUsers() {}
@apiEndpoint('POST', '/api/users')
static async createUser(userData) {}
@apiEndpoint('PUT', '/api/users/:id')
static async updateUser({ id, ...userData }) {}
}
// 使用示例
async function main() {
const users = await UserService.getUsers();
const newUser = await UserService.createUser({ name: 'Alice' });
const updatedUser = await UserService.updateUser({ id: 1, name: 'Bob' });
}
元数据反射与装饰器
结合Reflect Metadata提案,装饰器可以实现更强大的元编程能力。首先需要安装reflect-metadata
包:
npm install reflect-metadata
然后可以使用类型元数据:
import 'reflect-metadata';
function type(type) {
return function(target, key) {
Reflect.defineMetadata('design:type', type, target, key);
};
}
function validate(target, name, descriptor) {
const originalMethod = descriptor.value;
descriptor.value = function(...args) {
const paramTypes = Reflect.getMetadata('design:paramtypes', target, name);
args.forEach((arg, index) => {
const expectedType = paramTypes[index];
if (arg !== null && arg !== undefined && !(arg instanceof expectedType)) {
throw new Error(`Parameter ${index} must be of type ${expectedType.name}`);
}
});
return originalMethod.apply(this, args);
};
return descriptor;
}
class MathService {
@validate
add(@type(Number) a, @type(Number) b) {
return a + b;
}
}
const math = new MathService();
console.log(math.add(2, 3)); // 5
console.log(math.add('2', 3)); // Error: Parameter 0 must be of type Number
装饰器组合实践
多个装饰器可以组合使用,创建复杂的行为。下面是一个结合日志、缓存和验证的示例:
function log() {
return function(target, name, descriptor) {
const originalMethod = descriptor.value;
descriptor.value = function(...args) {
console.log(`Calling ${name} with args:`, args);
const result = originalMethod.apply(this, args);
console.log(`Method ${name} returned:`, result);
return result;
};
return descriptor;
};
}
function validateArgs(...validators) {
return function(target, name, descriptor) {
const originalMethod = descriptor.value;
descriptor.value = function(...args) {
validators.forEach((validator, index) => {
if (!validator(args[index])) {
throw new Error(`Argument ${index} is invalid`);
}
});
return originalMethod.apply(this, args);
};
return descriptor;
};
}
class ProductService {
@log()
@validateArgs(
val => typeof val === 'string',
val => typeof val === 'number' && val > 0
)
createProduct(name, price) {
return { id: Date.now(), name, price };
}
}
const service = new ProductService();
const product = service.createProduct('Laptop', 999);
// 控制台输出调用和返回日志
service.createProduct('', -1); // 抛出验证错误
装饰器在React中的应用
虽然React组件通常是函数组件,但类组件仍然可以使用装饰器。下面是一个高阶组件装饰器示例:
function withLoading(WrappedComponent) {
return class extends React.Component {
state = { loading: true };
async componentDidMount() {
if (typeof this.props.loadData === 'function') {
await this.props.loadData();
}
this.setState({ loading: false });
}
render() {
if (this.state.loading) {
return <div>Loading...</div>;
}
return <WrappedComponent {...this.props} />;
}
};
}
@withLoading
class UserProfile extends React.Component {
render() {
return <div>{this.props.user.name}'s Profile</div>;
}
}
// 使用
<UserProfile
user={{ name: 'Alice' }}
loadData={async () => {
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 1000));
}}
/>
装饰器的局限性
虽然装饰器功能强大,但也存在一些限制:
- 不能装饰函数(非类方法)
- 属性装饰器不能直接修改属性值
- 静态方法和原型方法的装饰器参数不同
- 目前仍是Stage 3提案,语法可能变化
- 过度使用可能导致代码难以理解
性能考量
装饰器在运行时会有一定的性能开销,特别是在频繁调用的方法上。应当注意:
- 避免在装饰器中进行复杂计算
- 对于高频调用的方法,考虑是否真的需要装饰器
- 装饰器工厂只在初始化时执行一次,但装饰器函数会在每次访问时执行
// 低效的装饰器示例
function inefficientDecorator() {
// 这个复杂计算会在每次装饰器调用时执行
const heavyCalculation = Array(1000000).fill(0).map((_, i) => i * i);
return function(target, name, descriptor) {
// ...
};
}
TypeScript中的装饰器
TypeScript已经支持装饰器语法(需要启用experimentalDecorators
选项)。TypeScript装饰器与ES7提案略有不同,但概念相似。TypeScript还提供了额外的元数据能力:
// tsconfig.json
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
// 使用示例
import 'reflect-metadata';
function logType(target: any, key: string) {
const type = Reflect.getMetadata('design:type', target, key);
console.log(`${key} type: ${type.name}`);
}
class Example {
@logType
public name: string = 'test';
@logType
public age: number = 30;
}
// 控制台输出:
// name type: String
// age type: Number