在 JavaScript 里,装饰器(Decorators)是一种能对类、方法、属性的行为进行扩展或者修改的语法。它的核心原理是借助元编程,在不改变原有代码结构的前提下,为目标添加新功能。
基本概念
直接show code,现有如下代码,用来记录log日志:
function log(target, name, descriptor) {
const original = descriptor.value;
descriptor.value = function(...args) {
console.log(`调用 ${name} 方法,参数:${JSON.stringify(args)}`);
const result = original.apply(this, args);
console.log(`方法 ${name} 返回:${result}`);
return result;
};
return descriptor;
}
class Calculator {
@log
add(a, b) {
return a + b;
}
}
// 使用示例
const calc = new Calculator();
calc.add(3, 4); // 控制台会输出调用信息和返回结果
装饰器函数参数解析
在 JavaScript 装饰器中,log
函数的三个参数分别代表:
-
target
:被装饰的类或原型对象。
- 若装饰的是类方法,
target
就是类的原型(prototype
)。
- 若装饰的是类,
target
就是类本身。
-
name
:被装饰的方法或属性的名称(字符串类型)。
-
descriptor
:属性描述符对象(与 Object.defineProperty
中的描述符相同),包含以下属性:
-
value
:被装饰的方法或属性的值(即原始函数)。
-
writable
:是否可修改(布尔值)。
-
enumerable
:是否可枚举(布尔值)。
-
configurable
:是否可配置(布尔值)。
函数实现原理详解
log
装饰器的核心逻辑是替换原始方法,在执行前后添加日志:
function log(target, name, descriptor) {
// 1. 保存原始方法的引用
const original = descriptor.value;
// 2. 修改 descriptor.value 为新函数
descriptor.value = function(...args) {
// 3. 执行前置逻辑(打印入参)
console.log(`调用 ${name} 方法,参数:${JSON.stringify(args)}`);
// 4. 执行原始方法并保存结果
const result = original.apply(this, args);
// 5. 执行后置逻辑(打印返回值)
console.log(`方法 ${name} 返回:${result}`);
// 6. 返回原始结果
return result;
};
// 7. 返回修改后的描述符
return descriptor;
}
为什么要这样实现?
这种写法的关键点在于:
-
不改变原始方法的核心逻辑:通过包装原始方法,在不修改其代码的前提下添加新功能。
-
保留上下文(this
) :
- 使用
original.apply(this, args)
确保原始方法在调用时的 this
指向不变。
- 若直接调用
original(args)
,可能导致 this
指向全局对象(非严格模式)或 undefined
(严格模式)。
-
支持任意参数:
- 使用剩余参数
...args
收集所有传入参数。
- 使用
JSON.stringify(args)
将参数序列化为字符串(需注意无法处理函数或 undefined
类型的参数)。
-
遵循装饰器规范:
- 装饰器必须返回一个描述符对象(或新类)。
- 通过修改
descriptor.value
替换原始方法。
应用示例
使用该装饰器的类方法会自动添加日志功能:
class Calculator {
@log
add(a, b) {
return a + b;
}
}
const calc = new Calculator();
calc.add(3, 4);
// 输出:
// 调用 add 方法,参数:[3,4]
// 方法 add 返回:7
注意事项
-
参数序列化限制:
-
JSON.stringify
无法处理函数或 undefined
参数,可能导致日志不完整。
- 改进方案:使用
args.map(arg => String(arg)).join(', ')
或自定义序列化函数。
-
异步方法处理:
-
若原始方法返回 Promise,需使用 await
等待结果:
descriptor.value = async function(...args) {
// ...
const result = await original.apply(this, args);
// ...
};
-
兼容性:
通过这种方式,装饰器实现了 ** 横切关注点(Cross-cutting Concerns)** 的分离,让日志、权限等功能与核心业务逻辑解耦。
下面介绍装饰器常见的应用场景:
1. 日志记录
装饰器能够在方法执行的前后添加日志,这样可以对函数的调用情况进行监控。
function log(target, name, descriptor) {
const original = descriptor.value;
descriptor.value = function(...args) {
console.log(`调用 ${name} 方法,参数:${JSON.stringify(args)}`);
const result = original.apply(this, args);
console.log(`方法 ${name} 返回:${result}`);
return result;
};
return descriptor;
}
class Calculator {
@log
add(a, b) {
return a + b;
}
}
// 使用示例
const calc = new Calculator();
calc.add(3, 4); // 控制台会输出调用信息和返回结果
2. 权限验证
可以在执行方法前对用户权限进行检查,防止未授权的访问。
function auth(requiredRole) {
return function(target, name, descriptor) {
const original = descriptor.value;
descriptor.value = function(...args) {
if (this.userRole !== requiredRole) {
throw new Error("权限不足");
}
return original.apply(this, args);
};
return descriptor;
};
}
class AdminPanel {
userRole = "admin";
@auth("admin")
deleteUser() {
return "用户已删除";
}
}
3. 性能分析
装饰器能够对函数的执行时间进行测量,有助于性能优化。
function benchmark(target, name, descriptor) {
const original = descriptor.value;
descriptor.value = async function(...args) {
const start = performance.now();
const result = await original.apply(this, args);
const end = performance.now();
console.log(`${name} 方法执行耗时:${end - start}ms`);
return result;
};
return descriptor;
}
class DataService {
@benchmark
async fetchData() {
await new Promise(resolve => setTimeout(resolve, 1000));
return { data: "大量数据" };
}
}
4. 自动绑定
在 React 等框架中,装饰器可以解决方法上下文丢失的问题。
function autobind(target, name, descriptor) {
const original = descriptor.value;
return {
configurable: true,
get() {
const bound = original.bind(this);
Object.defineProperty(this, name, {
value: bound,
configurable: true,
writable: true
});
return bound;
}
};
}
class Component {
constructor() {
this.state = { count: 0 };
}
@autobind
increment() {
this.state.count++;
}
}
5. 单例模式实现
装饰器可以确保一个类仅有一个实例。
function singleton(constructor) {
let instance;
return function(...args) {
if (!instance) {
instance = new constructor(...args);
}
return instance;
};
}
@singleton
class AppState {
constructor() {
this.data = {};
}
}
const state1 = new AppState();
const state2 = new AppState();
console.log(state1 === state2); // 输出 true
6. 类型检查
在运行时对函数参数的类型进行验证。
function validateTypes(target, name, descriptor) {
const original = descriptor.value;
descriptor.value = function(...args) {
const paramTypes = Reflect.getMetadata("design:paramtypes", target, name);
args.forEach((arg, i) => {
if (arg && paramTypes[i] && !(arg instanceof paramTypes[i])) {
throw new TypeError(`参数 ${i} 类型错误,期望 ${paramTypes[i].name}`);
}
});
return original.apply(this, args);
};
return descriptor;
}
class MathUtils {
@validateTypes
add(a: number, b: number) {
return a + b;
}
}
7. 缓存机制
对函数的计算结果进行缓存,避免重复计算。
function memoize(target, name, descriptor) {
const original = descriptor.value;
const cache = new Map();
descriptor.value = function(...args) {
const key = args.toString();
if (cache.has(key)) {
return cache.get(key);
}
const result = original.apply(this, args);
cache.set(key, result);
return result;
};
return descriptor;
}
class Fibonacci {
@memoize
calculate(n) {
return n <= 1 ? n : this.calculate(n - 1) + this.calculate(n - 2);
}
}
装饰器使用注意要点
-
要启用装饰器语法,需要在 Babel 或者 TypeScript 中进行配置。
-
装饰器的执行顺序是从下往上,例如:
@a
@b
method() {} // 先执行 b,再执行 a
-
装饰器可以返回一个新的类或者修改原有的描述符(descriptor)。
装饰器的主要价值在于它遵循了开放 - 封闭原则,即对扩展开放,对修改封闭。它能让代码变得更加简洁,同时增强代码的可复用性。