二、Decorators 提案历史
2.1 Stage 1(旧版)
function log(target, name, descriptor) {
const original = descriptor.value;
descriptor.value = function(...args) {
console.log(`Calling ${name}`);
return original.apply(this, args);
};
return descriptor;
}
class MyClass {
@log
myMethod() {}
}
2.2 Stage 3(新版 2022-03)
function logged(originalMethod, context) {
const { kind, name } = context;
if (kind === 'method') {
return function(...args) {
console.log(`Calling ${String(name)}`);
return originalMethod.apply(this, args);
};
}
}
class MyClass {
@logged
myMethod() {}
}
三、装饰器类型
3.1 类装饰器
function sealed(classDefinition, context) {
if (context.kind === 'class') {
Object.seal(classDefinition);
Object.seal(classDefinition.prototype);
}
}
@sealed
class MyClass {
constructor() {}
}
3.2 方法装饰器
function logged(method, context) {
if (context.kind === 'method') {
const methodName = context.name;
return function(...args) {
console.log(`Calling ${String(methodName)}`);
return method.apply(this, args);
};
}
}
class Calculator {
@logged
add(a, b) {
return a + b;
}
}
3.3 Getter/Setter 装饰器
function immutable(getter, context) {
if (context.kind === 'getter') {
const { name } = context;
return function() {
const value = getter.call(this);
return Object.freeze(value);
};
}
}
class User {
#data = { name: 'John' };
@immutable
get data() {
return this.#data;
}
}
3.4 字段装饰器
function bound(value, context) {
if (context.kind === 'field') {
const { name } = context;
context.addInitializer(function() {
this[name] = this[name].bind(this);
});
}
}
class Button {
@bound
handleClick() {
console.log(this);
}
}
3.5 Accessor 装饰器
function deprecated(accessor, context) {
const { name, kind } = context;
if (kind === 'accessor') {
return {
get() {
console.warn(`Accessing deprecated ${String(name)}`);
return accessor.get.call(this);
},
set(value) {
console.warn(`Setting deprecated ${String(name)}`);
return accessor.set.call(this, value);
}
};
}
}
class MyClass {
@deprecated
accessor oldValue;
}
四、装饰器工厂
4.1 带参数的装饰器
function log(message) {
return function(originalMethod, context) {
return function(...args) {
console.log(message);
return originalMethod.apply(this, args);
};
};
}
class MyClass {
@log('Method called')
myMethod() {}
}
4.2 工厂函数示例
function measure(enabled = true) {
return function(method, context) {
if (!enabled) return method;
const name = context.name;
return function(...args) {
console.time(name);
const result = method.apply(this, args);
console.timeEnd(name);
return result;
};
};
}
class DataProcessor {
@measure(true)
processLargeData() {
}
}
五、实战案例
5.1 案例一:日志装饰器
function logger(target, context) {
if (context.kind === 'method') {
const methodName = context.name;
return function(...args) {
console.log(`[LOG] ${String(methodName)} called with:`, args);
const result = target.apply(this, args);
console.log(`[LOG] ${String(methodName)} returned:`, result);
return result;
};
}
}
class UserService {
@logger
getUser(id) {
return { id, name: 'User' };
}
}
5.2 案例二:缓存装饰器
function cache(fn, context) {
if (context.kind === 'method') {
const cacheMap = new Map();
return function(...args) {
const key = JSON.stringify(args);
if (cacheMap.has(key)) {
console.log('Cache hit');
return cacheMap.get(key);
}
const result = fn.apply(this, args);
cacheMap.set(key, result);
return result;
};
}
}
class APIService {
@cache
fetchData(id) {
console.log('Fetching from API');
return { data: id };
}
}
5.3 案例三:验证装饰器
function validate(fn, context) {
if (context.kind === 'method') {
return function(...args) {
if (args.some(arg => arg === null || arg === undefined)) {
throw new Error('Arguments cannot be null or undefined');
}
return fn.apply(this, args);
};
}
}
class Calculator {
@validate
divide(a, b) {
if (b === 0) throw new Error('Division by zero');
return a / b;
}
}
5.4 案例四:自动绑定
function autobind(method, context) {
if (context.kind === 'method') {
const { name } = context;
context.addInitializer(function() {
this[name] = this[name].bind(this);
});
}
}
class Button {
constructor() {
this.text = 'Click';
}
@autobind
handleClick() {
console.log(this.text);
}
}
const button = new Button();
const handler = button.handleClick;
handler();
六、多个装饰器组合
6.1 装饰器执行顺序
function decorator1(fn, context) {
console.log('Decorator 1');
return fn;
}
function decorator2(fn, context) {
console.log('Decorator 2');
return fn;
}
class MyClass {
@decorator1
@decorator2
method() {
console.log('Method');
}
}
6.2 组合示例
class APIService {
@logger
@cache
@validate
fetchData(id) {
return { data: id };
}
}
七、TypeScript 中的装饰器
7.1 启用装饰器
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
7.2 TypeScript 装饰器
function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = function(...args: any[]) {
console.log(`Calling ${propertyKey}`);
return original.apply(this, args);
};
}
class MyClass {
@log
myMethod() {}
}
八、装饰器与现有代码集成
8.1 Babel 配置
{
"plugins": [
["@babel/plugin-proposal-decorators", { "version": "2023-05" }]
]
}
8.2 TypeScript 迁移
function oldDecorator(target, name, descriptor) {}
function newDecorator(method, context) {}
九、最佳实践
- 保持装饰器纯粹:不要有副作用
- 提供良好的错误信息
- 文档完善
- 测试覆盖
- 考虑性能
- 合理使用组合