各位听众,各位编程爱好者,大家好!
今天,我们将深入探讨 JavaScript 中一个强大而优雅的编程范式——装饰器(Decorator)。你可能听说过它,或者在其他语言(如 Python、Java)中见过类似的概念。在 JavaScript 中,装饰器目前仍处于提案阶段,但它所代表的元编程(metaprogramming)思想以及对代码行为的修改能力,是每个高级开发者都应掌握的。
我们将聚焦于如何“手写实现”一个装饰器函数,更准确地说,是如何利用 JavaScript 强大的 Proxy 和 Reflect API,来模拟并实现修改类属性或方法行为的装饰器功能。这不仅仅是为了提前体验未来的语法,更是为了深入理解 Proxy 和 Reflect 的强大能力,以及它们在构建高级抽象和框架中的应用。
装饰器:代码行为的织补匠
什么是装饰器?
在软件设计模式中,装饰器模式(Decorator Pattern)允许你在不改变原有对象结构的前提下,动态地给一个对象添加一些额外的职责或行为。它通常通过将对象包装在一个装饰器类中来实现,该装饰器类具有与原始对象相同的接口,并在调用原始对象的方法前后执行一些额外的逻辑。
在编程语言的语法层面,装饰器通常表现为一种特殊的语法糖,允许你将函数或类“标记”为被另一个函数“装饰”。这个“装饰”函数会在编译时或运行时接收被标记的实体(类、方法、属性等),并返回一个修改后的版本。
为什么我们需要装饰器?
装饰器带来了许多好处,其中最主要的是:
- 关注点分离 (Separation of Concerns):将横切关注点(如日志、性能监控、权限验证、缓存)从核心业务逻辑中剥离出来,提高代码的内聚性和可维护性。
- 代码复用 (Code Reusability):一次编写,多处使用。通过装饰器封装通用逻辑,避免重复代码。
- 可读性和表达力 (Readability and Expressiveness):使用简洁的语法标记,清晰地表达某个类、方法或属性的额外行为。
- 元编程 (Metaprogramming):在运行时或编译时修改程序的结构或行为,实现高度灵活和动态的系统。
JavaScript 装饰器现状与我们的目标
JavaScript 中的装饰器提案(Stage 3)正在稳步推进,它引入了 @ 符号的语法糖,例如:
@classDecorator
class MyClass {
@methodDecorator
myMethod() { /* ... */ }
@propertyDecorator
myProperty = 123;
}
然而,这套语法需要 Babel 等工具进行转译才能在当前环境中使用。
我们的目标不是模仿 @ 语法糖,而是深入其核心机制:如何通过函数包装和对象拦截,在运行时修改类、方法或属性的行为。我们将利用 Proxy 和 Reflect 这对强大的组合,在不依赖任何转译工具的情况下,手写实现这种行为修改的能力。这将帮助我们更好地理解装饰器的工作原理,并为将来使用原生装饰器打下坚实的基础。
基石:Proxy 和 Reflect
在深入实现装饰器之前,我们必须先掌握两个核心工具:Proxy 和 Reflect。它们是 JavaScript 中进行元编程的基石,允许我们拦截并自定义对象的基本操作。
Proxy:对象操作的拦截器
Proxy 对象用于创建一个对象的代理,从而允许你拦截并自定义对该对象的基本操作(例如属性查找、赋值、方法调用等)。
构造函数:
new Proxy(target, handler);
target:要代理的目标对象(可以是任何对象,包括函数、数组、甚至另一个 Proxy)。handler:一个对象,其属性是用于定义代理行为的“陷阱”(trap)函数。
核心思想: 当你对 Proxy 实例进行操作时,这些操作不会直接作用于 target,而是会先被 handler 对象中对应的“陷阱”函数捕获。你可以在陷阱函数中执行自定义逻辑,然后决定是继续执行原始操作,还是完全改变其行为。
常用陷阱(Traps):
| 陷阱名称 | 描述 | 参数 | 典型应用 |
|---|---|---|---|
get(target, prop, receiver) |
拦截对象属性的读取操作。 | target: 目标对象,prop: 属性名,receiver: Proxy 实例或继承 Proxy 的对象 |
数据验证、默认值、计算属性、访问控制 |
set(target, prop, value, receiver) |
拦截对象属性的设置操作。 | target: 目标对象,prop: 属性名,value: 新值,receiver: Proxy 实例 |
数据验证、副作用、追踪属性修改、只读属性 |
apply(target, thisArg, argumentsList) |
拦截函数调用。当 target 是一个函数时,且 Proxy 作为函数被调用。 |
target: 目标函数,thisArg: 调用时的 this,argumentsList: 参数数组 |
性能监控、参数校验、缓存、函数柯里化 |
construct(target, argumentsList, newTarget) |
拦截 new 操作。当 target 是一个构造函数时,且 Proxy 作为构造函数被调用。 |
target: 目标构造函数,argumentsList: 构造函数的参数,newTarget: 原始 new 操作符的目标 |
实例初始化修改、工厂模式、单例模式 |
defineProperty(target, prop, descriptor) |
拦截 Object.defineProperty()。 |
target: 目标对象,prop: 属性名,descriptor: 属性描述符 |
阻止属性定义、修改属性描述符 |
deleteProperty(target, prop) |
拦截 delete 操作。 |
target: 目标对象,prop: 属性名 |
阻止属性删除、日志记录 |
has(target, prop) |
拦截 in 操作符。 |
target: 目标对象,prop: 属性名 |
隐藏私有属性、权限检查 |
getOwnPropertyDescriptor(target, prop) |
拦截 Object.getOwnPropertyDescriptor()。 |
target: 目标对象,prop: 属性名 |
修改属性描述符 |
getPrototypeOf(target) |
拦截 Object.getPrototypeOf()。 |
target: 目标对象 |
修改原型链 |
isExtensible(target) |
拦截 Object.isExtensible()。 |
target: 目标对象 |
阻止扩展对象 |
ownKeys(target) |
拦截 Object.keys(), Object.getOwnPropertyNames(), Object.getOwnPropertySymbols()。 |
target: 目标对象 |
过滤可枚举属性 |
preventExtensions(target) |
拦截 Object.preventExtensions()。 |
target: 目标对象 |
阻止扩展对象 |
setPrototypeOf(target, prototype) |
拦截 Object.setPrototypeOf()。 |
target: 目标对象,prototype: 新原型 |
限制原型链修改 |
示例:一个简单的属性访问日志代理
const user = {
name: 'Alice',
age: 30
};
const userProxy = new Proxy(user, {
get(target, prop, receiver) {
console.log(`[Proxy Log] Accessing property: ${String(prop)}`);
// 使用 Reflect 转发操作到目标对象,确保正确的 `this` 上下文
return Reflect.get(target, prop, receiver);
},
set(target, prop, value, receiver) {
console.log(`[Proxy Log] Setting property: ${String(prop)} to ${value}`);
if (prop === 'age' && value < 0) {
console.warn('Age cannot be negative!');
return false; // 阻止设置
}
return Reflect.set(target, prop, value, receiver);
}
});
console.log(userProxy.name); // 输出日志并返回 'Alice'
userProxy.age = 31; // 输出日志
userProxy.age = -5; // 输出警告并阻止设置
console.log(userProxy.age); // 31
Reflect:操作对象的标准库
Reflect 是一个内置对象,提供拦截 JavaScript 操作的方法。这些方法与 Proxy 的陷阱方法一一对应,且行为与默认操作相同。
为什么需要 Reflect?
- 标准化操作:
Reflect提供了一套统一的 API 来执行对象操作,避免了直接使用Object.prototype上的方法(例如Object.defineProperty),这些方法在某些情况下行为不一致,或者需要手动处理this上下文。 this上下文的正确性: 在Proxy陷阱中,使用Reflect转发操作可以确保this上下文始终指向原始操作的对象(即receiver参数),这对于继承和复杂对象结构至关重要。- 返回布尔值:
Reflect的一些方法(如Reflect.set,Reflect.deleteProperty)会返回一个布尔值,表示操作是否成功,这比某些Object方法抛出错误更易于处理。
常用 Reflect 方法:
Reflect 方法 |
对应 Proxy 陷阱 |
描述 |
|---|---|---|
Reflect.get(target, prop, receiver) |
get(target, prop, receiver) |
获取对象属性的值。 |
Reflect.set(target, prop, value, receiver) |
set(target, prop, value, receiver) |
设置对象属性的值。 |
Reflect.apply(target, thisArg, argumentsList) |
apply(target, thisArg, argumentsList) |
调用函数。 |
Reflect.construct(target, argumentsList, newTarget) |
construct(target, argumentsList, newTarget) |
使用构造函数创建实例。 |
Reflect.defineProperty(target, prop, descriptor) |
defineProperty(target, prop, descriptor) |
定义对象属性。 |
Reflect.deleteProperty(target, prop) |
deleteProperty(target, prop) |
删除对象属性。 |
Reflect.has(target, prop) |
has(target, prop) |
检查对象是否包含某个属性。 |
Reflect.getOwnPropertyDescriptor(target, prop) |
getOwnPropertyDescriptor(target, prop) |
获取对象自有属性的描述符。 |
Reflect.getPrototypeOf(target) |
getPrototypeOf(target) |
获取对象的原型。 |
Reflect.isExtensible(target) |
isExtensible(target) |
检查对象是否可扩展。 |
Reflect.ownKeys(target) |
ownKeys(target) |
返回对象所有自有属性(包括不可枚举的 Symbol)的键。 |
Reflect.preventExtensions(target) |
preventExtensions(target) |
阻止扩展对象。 |
Reflect.setPrototypeOf(target, prototype) |
setPrototypeOf(target, prototype) |
设置对象的原型。 |
总结: Proxy 负责拦截操作,Reflect 负责在拦截后执行(或转发)默认的操作,并提供更健壮的 API。它们是协同工作的最佳搭档。
手写实现类装饰器
类装饰器用于修改整个类的行为,通常是在类定义之后对其构造函数进行包装。
场景一:日志记录类实例化行为
假设我们想在每次类实例化时记录一条日志。
核心思路:
- 创建一个函数,它接收一个类(构造函数)作为参数。
- 返回一个新的构造函数,这个新构造函数在执行原始类的
new操作时,可以插入额外的逻辑。 Proxy的construct陷阱正是为此而生。
/**
* 类装饰器:记录类的实例化行为
* @param {Function} TargetClass - 要装饰的目标类(构造函数)
* @returns {Function} - 包装后的新类(构造函数)
*/
function logInstantiation(TargetClass) {
console.log(`[Decorator Init] Applying logInstantiation to class: ${TargetClass.name}`);
// 返回一个 Proxy 包装的类
return new Proxy(TargetClass, {
/**
* 拦截 new 操作,即类实例化
* @param {Function} target - 原始构造函数 (TargetClass)
* @param {Array} argumentsList - 构造函数的参数列表
* @param {Function} newTarget - 原始 new 操作符的目标 (通常就是 Proxy 本身)
*/
construct(target, argumentsList, newTarget) {
console.log(`[Instantiation Log] Instantiating ${target.name} with arguments:`, argumentsList);
// 使用 Reflect.construct 调用原始构造函数,创建实例
// 确保 `this` (即 newTarget) 正确传递,以便处理继承等情况
const instance = Reflect.construct(target, argumentsList, newTarget);
console.log(`[Instantiation Log] ${target.name} instance created:`, instance);
return instance;
}
});
}
// 应用装饰器
// 注意:由于我们没有 @ 语法糖,所以需要手动重新赋值
class UserService {
constructor(name) {
this.name = name;
this.id = Math.random().toString(36).substring(7);
}
getUserInfo() {
return `User: ${this.name}, ID: ${this.id}`;
}
}
// 手动应用装饰器函数
const LoggedUserService = logInstantiation(UserService);
console.log('n--- Testing LoggedUserService ---');
const user1 = new LoggedUserService('Alice'); // 会触发 Proxy.construct 陷阱
console.log(user1.getUserInfo());
const user2 = new LoggedUserService('Bob'); // 再次触发
console.log(user2.getUserInfo());
输出示例:
[Decorator Init] Applying logInstantiation to class: UserService
--- Testing LoggedUserService ---
[Instantiation Log] Instantiating UserService with arguments: [ 'Alice' ]
[Instantiation Log] UserService instance created: UserService { name: 'Alice', id: '...' }
User: Alice, ID: ...
[Instantiation Log] Instantiating UserService with arguments: [ 'Bob' ]
[Instantiation Log] UserService instance created: UserService { name: 'Bob', id: '...' }
User: Bob, ID: ...
场景二:为类添加静态属性或方法
类装饰器也可以在类定义时,为类本身(而不是实例)添加静态属性或方法。
核心思路:
- 装饰器函数接收目标类。
- 直接修改目标类(因为类本身也是对象)。
/**
* 类装饰器:为类添加静态属性和方法
* @param {Function} TargetClass - 要装饰的目标类
* @returns {Function} - 修改后的类
*/
function addStaticMembers(TargetClass) {
console.log(`[Decorator Init] Applying addStaticMembers to class: ${TargetClass.name}`);
// 添加静态属性
TargetClass.version = '1.0.0';
// 添加静态方法
TargetClass.getAppName = function() {
return `Application Name: ${TargetClass.name} App`;
};
TargetClass.prototype.getCreationTime = function() {
return `Created at: ${new Date().toLocaleString()}`;
};
return TargetClass; // 直接返回修改后的类
}
class ProductService {
constructor(productName) {
this.productName = productName;
}
getProductDetails() {
return `Product: ${this.productName}`;
}
}
const EnhancedProductService = addStaticMembers(ProductService);
console.log('n--- Testing EnhancedProductService ---');
console.log(`Version: ${EnhancedProductService.version}`);
console.log(EnhancedProductService.getAppName());
const product = new EnhancedProductService('Laptop');
console.log(product.getProductDetails());
console.log(product.getCreationTime()); // 实例方法也添加成功
输出示例:
[Decorator Init] Applying addStaticMembers to class: ProductService
--- Testing EnhancedProductService ---
Version: 1.0.0
Application Name: ProductService App
Product: Laptop
Created at: 2023/10/27 下午3:45:00
手写实现方法装饰器
方法装饰器用于修改类中特定方法的行为。这通常涉及对方法的调用进行拦截和包装。
关键挑战与解决方案
挑战: 方法是存储在类的原型 (prototype) 上的函数。我们不能直接修改类的原型,因为那会影响所有实例。我们需要的是在方法被调用时进行拦截。
解决方案:
- 方法装饰器接收目标类的原型对象和方法名。
- 它需要获取原始方法,并返回一个新的函数来替换原始方法。
- 这个新函数会包含我们额外的逻辑,并在适当的时候调用原始方法。
- 为了正确处理
this上下文,我们会使用Reflect.apply来调用原始方法。
场景一:性能监控(记录方法执行时间)
/**
* 方法装饰器:记录方法执行时间
* @param {Object} target - 类的原型对象 (e.g., MyClass.prototype)
* @param {string} methodName - 要装饰的方法的名称
* @param {PropertyDescriptor} descriptor - 方法的属性描述符
* @returns {PropertyDescriptor} - 修改后的属性描述符
*/
function performanceLogger(target, methodName, descriptor) {
// 确保 descriptor.value 是一个函数
if (typeof descriptor.value !== 'function') {
throw new Error(`@performanceLogger can only be applied to methods, not properties.`);
}
const originalMethod = descriptor.value; // 获取原始方法
console.log(`[Decorator Init] Applying performanceLogger to method: ${methodName} of class: ${target.constructor.name}`);
// 返回一个新的属性描述符,其 value 是一个包装函数
return {
...descriptor, // 保留原始描述符的其他属性 (enumerable, configurable, writable)
value: function(...args) {
const start = performance.now(); // 记录开始时间
// 使用 Reflect.apply 调用原始方法
// target: 原始方法
// this: 确保方法内部的 this 指向当前实例
// args: 传递所有参数
const result = Reflect.apply(originalMethod, this, args);
const end = performance.now(); // 记录结束时间
console.log(`[Performance Log] Method "${methodName}" executed in ${end - start} ms with args:`, args);
return result; // 返回原始方法的执行结果
}
};
}
// --- 应用方法装饰器 ---
class DataProcessor {
constructor(data) {
this.data = data;
}
// 模拟 @performanceLogger 语法
// 实际应用时,需要手动调用装饰器函数并重新定义属性
// DataProcessor.prototype.processData = performanceLogger(
// DataProcessor.prototype,
// 'processData',
// Object.getOwnPropertyDescriptor(DataProcessor.prototype, 'processData')
// ).value;
// 或者更简洁地:
// const originalDescriptor = Object.getOwnPropertyDescriptor(DataProcessor.prototype, 'processData');
// const newDescriptor = performanceLogger(DataProcessor.prototype, 'processData', originalDescriptor);
// Object.defineProperty(DataProcessor.prototype, 'processData', newDescriptor);
processData(iterations) {
let sum = 0;
for (let i = 0; i < iterations; i++) {
sum += i * Math.random();
}
return `Processed sum: ${sum.toFixed(2)}`;
}
// 另一个方法
calculateAverage(numbers) {
if (!numbers || numbers.length === 0) return 0;
const sum = numbers.reduce((acc, num) => acc + num, 0);
return sum / numbers.length;
}
}
// 手动应用 performanceLogger 装饰器到 processData 方法
const originalProcessDataDescriptor = Object.getOwnPropertyDescriptor(DataProcessor.prototype, 'processData');
const newProcessDataDescriptor = performanceLogger(
DataProcessor.prototype,
'processData',
originalProcessDataDescriptor
);
Object.defineProperty(DataProcessor.prototype, 'processData', newProcessDataDescriptor);
// 手动应用 performanceLogger 装饰器到 calculateAverage 方法
const originalCalculateAverageDescriptor = Object.getOwnPropertyDescriptor(DataProcessor.prototype, 'calculateAverage');
const newCalculateAverageDescriptor = performanceLogger(
DataProcessor.prototype,
'calculateAverage',
originalCalculateAverageDescriptor
);
Object.defineProperty(DataProcessor.prototype, 'calculateAverage', newCalculateAverageDescriptor);
console.log('n--- Testing DataProcessor with performanceLogger ---');
const processor = new DataProcessor([1, 2, 3, 4, 5]);
processor.processData(1000000); // 会触发性能日志
processor.calculateAverage([10, 20, 30]); // 也会触发性能日志
processor.calculateAverage([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
输出示例:
[Decorator Init] Applying performanceLogger to method: processData of class: DataProcessor
[Decorator Init] Applying performanceLogger to method: calculateAverage of class: DataProcessor
--- Testing DataProcessor with performanceLogger ---
[Performance Log] Method "processData" executed in 3.45 ms with args: [ 1000000 ]
[Performance Log] Method "calculateAverage" executed in 0.05 ms with args: [ [ 10, 20, 30 ] ]
[Performance Log] Method "calculateAverage" executed in 0.06 ms with args: [ [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ] ]
场景二:参数验证
方法装饰器可以用于在方法执行前对传入的参数进行验证。
/**
* 方法装饰器:参数验证
* @param {Array<Function>} validators - 验证器函数数组,每个函数接收一个参数并返回布尔值
* 或抛出错误。数组索引对应参数索引。
* @returns {Function} - 一个工厂函数,返回属性描述符修改器。
*/
function validateArgs(...validators) {
// 这是一个装饰器工厂,它返回真正的装饰器函数
return function(target, methodName, descriptor) {
if (typeof descriptor.value !== 'function') {
throw new Error(`@validateArgs can only be applied to methods.`);
}
const originalMethod = descriptor.value;
console.log(`[Decorator Init] Applying validateArgs to method: ${methodName} of class: ${target.constructor.name}`);
return {
...descriptor,
value: function(...args) {
// 遍历每个参数,应用对应的验证器
for (let i = 0; i < validators.length; i++) {
const validator = validators[i];
if (typeof validator === 'function') {
try {
if (!validator(args[i])) { // 如果验证器返回 false
throw new Error(`Validation failed for argument ${i} in method "${methodName}".`);
}
} catch (e) {
// 如果验证器抛出错误,则重新抛出
throw new Error(`Argument ${i} validation error in method "${methodName}": ${e.message}`);
}
}
}
// 所有验证通过,调用原始方法
return Reflect.apply(originalMethod, this, args);
}
};
};
}
// --- 验证器函数 ---
const isNumber = (val) => typeof val === 'number';
const isPositive = (val) => val > 0;
const isString = (val) => typeof val === 'string';
const isNotEmpty = (val) => val !== null && val !== undefined && val.trim() !== '';
class PaymentGateway {
processPayment(amount, currency, description) {
console.log(`Processing payment: ${currency} ${amount} for "${description}"`);
// 模拟支付逻辑
return true;
}
refund(transactionId, reason) {
console.log(`Refunding transaction ${transactionId} for reason: "${reason}"`);
return true;
}
}
// 手动应用 validateArgs 装饰器
const paymentProcessDescriptor = Object.getOwnPropertyDescriptor(PaymentGateway.prototype, 'processPayment');
const newPaymentProcessDescriptor = validateArgs(isNumber, isNotEmpty, isString)(
PaymentGateway.prototype,
'processPayment',
paymentProcessDescriptor
);
Object.defineProperty(PaymentGateway.prototype, 'processPayment', newPaymentProcessDescriptor);
const refundDescriptor = Object.getOwnPropertyDescriptor(PaymentGateway.prototype, 'refund');
const newRefundDescriptor = validateArgs(isString, isNotEmpty)(
PaymentGateway.prototype,
'refund',
refundDescriptor
);
Object.defineProperty(PaymentGateway.prototype, 'refund', newRefundDescriptor);
console.log('n--- Testing PaymentGateway with validateArgs ---');
const gateway = new PaymentGateway();
// 有效调用
gateway.processPayment(100, 'USD', 'Online purchase');
// 无效调用:金额不是数字
try {
gateway.processPayment('abc', 'USD', 'Test');
} catch (e) {
console.error(e.message);
}
// 无效调用:货币为空
try {
gateway.processPayment(50, '', 'Test');
} catch (e) {
console.error(e.message);
}
// 有效调用
gateway.refund('TXN12345', 'Customer requested');
// 无效调用:交易ID不是字符串
try {
gateway.refund(12345, 'Wrong ID format');
} catch (e) {
console.error(e.message);
}
输出示例:
[Decorator Init] Applying validateArgs to method: processPayment of class: PaymentGateway
[Decorator Init] Applying validateArgs to method: refund of class: PaymentGateway
--- Testing PaymentGateway with validateArgs ---
Processing payment: USD 100 for "Online purchase"
Argument 0 validation error in method "processPayment": Validation failed for argument 0 in method "processPayment".
Argument 1 validation error in method "processPayment": Validation failed for argument 1 in method "processPayment".
Refunding transaction TXN12345 for reason: "Customer requested"
Argument 0 validation error in method "refund": Validation failed for argument 0 in method "refund".
手写实现属性装饰器
属性装饰器用于修改类中特定属性的行为。这比方法装饰器稍微复杂一些,因为属性的访问(get)和设置(set)是不同的操作。
关键挑战与解决方案
挑战:
- 属性可能只是一个简单的值,也可能是一个 getter/setter。
- 我们希望在属性被读取或写入时进行拦截。
解决方案:
- 属性装饰器接收目标类的原型对象、属性名和属性描述符。
- 它需要返回一个新的属性描述符,其中包含自定义的
get和set陷阱。 - 如果原始属性没有 getter/setter,我们需要在新的
get/set中存储和检索实际值。
场景一:只读属性
使一个属性在初始化后不能被修改。
/**
* 属性装饰器:使属性只读
* @param {Object} target - 类的原型对象
* @param {string} propertyName - 属性的名称
* @param {PropertyDescriptor} descriptor - 属性的描述符(对于类字段,通常是 undefined)
* @returns {PropertyDescriptor} - 修改后的属性描述符
*/
function readOnly(target, propertyName, descriptor) {
console.log(`[Decorator Init] Applying readOnly to property: ${propertyName} of class: ${target.constructor.name}`);
// 如果 descriptor 存在,说明该属性可能已经有 getter/setter 或是一个配置过的属性
if (descriptor) {
// 强制设置为不可写
descriptor.writable = false;
// 如果有 setter,需要移除或修改,以确保只读性
if (descriptor.set) {
const originalSetter = descriptor.set;
descriptor.set = function(value) {
console.warn(`Attempted to write to read-only property "${propertyName}". Value will not be set.`);
// 可以选择抛出错误:throw new Error(`Cannot write to read-only property "${propertyName}"`);
// 或者调用原始 setter 但不改变值
// Reflect.apply(originalSetter, this, [value]);
};
}
return descriptor;
} else {
// 对于没有初始描述符的类字段(如 `myProperty = 'value'`),
// 它们在实例上被定义,我们需要在原型上定义一个 getter/setter
// 来控制所有实例的行为。或者更直接地,通过类装饰器包装实例。
// 在这里,我们假设它是一个在原型上被定义为只读的属性。
// 实际的类字段装饰器通常会以不同的方式处理,因为它们会在实例上创建。
// 为了模拟 ES 提案的行为,我们将为原型添加 getter/setter。
let value; // 闭包存储实际值
return {
configurable: true,
enumerable: true,
get() {
return value;
},
set(newValue) {
if (value !== undefined) { // 一旦设置过值,就不能再修改
console.warn(`Attempted to write to read-only property "${propertyName}". Value will not be set.`);
// throw new Error(`Cannot write to read-only property "${propertyName}"`);
} else {
value = newValue;
}
}
};
}
}
class Config {
// 模拟 @readOnly 语法
// 实际应用时,需要手动调用装饰器并定义属性
// Object.defineProperty(Config.prototype, 'API_KEY', readOnly(Config.prototype, 'API_KEY', undefined));
// Object.defineProperty(Config.prototype, 'MAX_RETRIES', readOnly(Config.prototype, 'MAX_RETRIES', { value: 5, writable: true, configurable: true, enumerable: true }));
API_KEY; // 这是一个类字段,会在实例上初始化
MAX_RETRIES = 5; // 这是一个带默认值的类字段
get VERSION() { return '1.0.0'; } // 这是一个 getter
}
// 手动应用 readOnly 装饰器
// 对于类字段,它们实际上是实例属性。
// 属性装饰器通常用于在类的原型上定义 getter/setter,从而影响所有实例。
// 要正确模拟类字段装饰器,我们通常需要一个类装饰器来包装整个实例的属性访问。
// 但为了演示单个属性装饰器,我们可以在原型上定义一个只读的 getter/setter
// 这会覆盖实例上的同名属性。或者,我们可以在 constructor 中拦截。
// 方案一:在原型上定义只读属性(会覆盖同名的实例属性)
// 这种方式更符合传统JS的defineProperty,但对于ES类字段行为有差异
const apiKeyDescriptor = readOnly(Config.prototype, 'API_KEY', undefined);
Object.defineProperty(Config.prototype, 'API_KEY', apiKeyDescriptor);
const maxRetriesDescriptor = readOnly(Config.prototype, 'MAX_RETRIES', { value: 5, writable: true, configurable: true, enumerable: true });
Object.defineProperty(Config.prototype, 'MAX_RETRIES', maxRetriesDescriptor);
// 对于 getter 属性,我们只需要修改其 descriptor.set
const versionDescriptor = Object.getOwnPropertyDescriptor(Config.prototype, 'VERSION');
const newVersionDescriptor = readOnly(Config.prototype, 'VERSION', versionDescriptor);
Object.defineProperty(Config.prototype, 'VERSION', newVersionDescriptor);
console.log('n--- Testing Config with readOnly ---');
const appConfig = new Config();
// 初始化只读属性
appConfig.API_KEY = 'my-secret-key-123'; // 第一次设置成功
console.log(`API_KEY: ${appConfig.API_KEY}`);
appConfig.MAX_RETRIES = 10; // 第一次设置成功
console.log(`MAX_RETRIES: ${appConfig.MAX_RETRIES}`);
// 尝试修改只读属性
appConfig.API_KEY = 'new-secret-key'; // 警告,不会修改
console.log(`API_KEY after attempt to change: ${appConfig.API_KEY}`);
appConfig.MAX_RETRIES = 20; // 警告,不会修改
console.log(`MAX_RETRIES after attempt to change: ${appConfig.MAX_RETRIES}`);
console.log(`VERSION: ${appConfig.VERSION}`);
try {
appConfig.VERSION = '2.0.0'; // 警告,不会修改
} catch (e) {
console.error(e.message);
}
console.log(`VERSION after attempt to change: ${appConfig.VERSION}`);
输出示例:
[Decorator Init] Applying readOnly to property: API_KEY of class: Config
[Decorator Init] Applying readOnly to property: MAX_RETRIES of class: Config
[Decorator Init] Applying readOnly to property: VERSION of class: Config
--- Testing Config with readOnly ---
API_KEY: my-secret-key-123
MAX_RETRIES: 10
Attempted to write to read-only property "API_KEY". Value will not be set.
API_KEY after attempt to change: my-secret-key-123
Attempted to write to read-only property "MAX_RETRIES". Value will not be set.
MAX_RETRIES after attempt to change: 10
VERSION: 1.0.0
Attempted to write to read-only property "VERSION". Value will not be set.
VERSION after attempt to change: 1.0.0
补充说明: 模拟原生类字段装饰器是一个挑战。原生装饰器对类字段的处理方式是,它们在字段初始化时被调用,可以返回一个新的字段值,或者返回一个属性描述符来控制未来的访问。
我们上面的 readOnly 示例通过在原型上定义 getter/setter 来实现,这会覆盖实例上可能存在的同名字段。更强大的方式是结合类装饰器,在实例创建时使用 Proxy 拦截所有属性访问。
场景二:数据转换(自动转换为大写)
在设置属性时自动将字符串值转换为大写。
/**
* 属性装饰器:自动将字符串属性值转换为大写
* @param {Object} target - 类的原型对象
* @param {string} propertyName - 属性的名称
* @param {PropertyDescriptor} descriptor - 属性的描述符
* @returns {PropertyDescriptor} - 修改后的属性描述符
*/
function uppercase(target, propertyName, descriptor) {
console.log(`[Decorator Init] Applying uppercase to property: ${propertyName} of class: ${target.constructor.name}`);
// 内部变量用于存储实际值
let value = descriptor ? descriptor.initializer ? descriptor.initializer.call(this) : descriptor.value : undefined;
return {
configurable: true,
enumerable: true,
get() {
return value;
},
set(newValue) {
if (typeof newValue === 'string') {
value = newValue.toUpperCase();
} else {
value = newValue; // 非字符串值不做转换
}
}
};
}
class UserProfile {
firstName = '';
lastName = '';
email = '';
}
// 手动应用 uppercase 装饰器
const firstNameDescriptor = uppercase(UserProfile.prototype, 'firstName', Object.getOwnPropertyDescriptor(UserProfile.prototype, 'firstName') || {initializer: () => ''});
Object.defineProperty(UserProfile.prototype, 'firstName', firstNameDescriptor);
const lastNameDescriptor = uppercase(UserProfile.prototype, 'lastName', Object.getOwnPropertyDescriptor(UserProfile.prototype, 'lastName') || {initializer: () => ''});
Object.defineProperty(UserProfile.prototype, 'lastName', lastNameDescriptor);
const emailDescriptor = uppercase(UserProfile.prototype, 'email', Object.getOwnPropertyDescriptor(UserProfile.prototype, 'email') || {initializer: () => ''});
Object.defineProperty(UserProfile.prototype, 'email', emailDescriptor);
console.log('n--- Testing UserProfile with uppercase ---');
const user = new UserProfile();
user.firstName = 'john';
user.lastName = 'doe';
user.email = '[email protected]';
console.log(`First Name: ${user.firstName}`);
console.log(`Last Name: ${user.lastName}`);
console.log(`Email: ${user.email}`);
user.firstName = 'jane';
user.lastName = 'smith';
user.email = '[email protected]';
console.log(`First Name: ${user.firstName}`);
console.log(`Last Name: ${user.lastName}`);
console.log(`Email: ${user.email}`);
user.firstName = 123; // 非字符串,不转换
console.log(`First Name (after non-string set): ${user.firstName}`);
输出示例:
[Decorator Init] Applying uppercase to property: firstName of class: UserProfile
[Decorator Init] Applying uppercase to property: lastName of class: UserProfile
[Decorator Init] Applying uppercase to property: email of class: UserProfile
--- Testing UserProfile with uppercase ---
First Name: JOHN
Last Name: DOE
Email: [email protected]
First Name: JANE
Last Name: SMITH
Email: [email protected]
First Name (after non-string set): 123
统一的装饰器工厂与应用策略
到目前为止,我们为不同类型的目标(类、方法、属性)编写了独立的装饰器函数。在实际应用中,我们可能需要一个更统一的机制来应用这些装饰器。
由于我们没有 @ 语法糖,手动应用装饰器需要一些策略。
策略一:手动链式调用或重新赋值
这是我们目前一直在做的方式:
// 类装饰器
class MyClass { /* ... */ }
MyClass = classDecorator(MyClass);
// 方法装饰器
const originalMethodDescriptor = Object.getOwnPropertyDescriptor(MyClass.prototype, 'myMethod');
const newMethodDescriptor = methodDecorator(MyClass.prototype, 'myMethod', originalMethodDescriptor);
Object.defineProperty(MyClass.prototype, 'myMethod', newMethodDescriptor);
// 属性装饰器
const originalPropertyDescriptor = Object.getOwnPropertyDescriptor(MyClass.prototype, 'myProperty');
const newPropertyDescriptor = propertyDecorator(MyClass.prototype, 'myProperty', originalPropertyDescriptor);
Object.defineProperty(MyClass.prototype, 'myProperty', newPropertyDescriptor);
这种方式明确,但对于大量装饰器来说会变得冗长。
策略二:一个通用的 applyDecorators 函数
我们可以编写一个辅助函数,来简化装饰器的应用。这个函数可以接收一个目标(类或实例)和一系列装饰器配置。
/**
* 辅助函数:应用装饰器
* 这是一个简化的版本,用于演示概念,实际生产环境可能需要更复杂的逻辑
* 来处理类字段(实例属性)和原型属性的差异。
*
* @param {Function|Object} target - 要装饰的类构造函数或类的原型对象
* @param {Array<Object>} decoratorsConfig - 装饰器配置数组
* 每个对象形如:{ type: 'class'|'method'|'property', name?: string, decorator: Function|FunctionFactory, args?: Array }
* @returns {Function|Object} - 装饰后的目标
*/
function applyDecorators(target, decoratorsConfig) {
let decoratedTarget = target;
for (const config of decoratorsConfig) {
if (config.type === 'class') {
// 类装饰器直接作用于构造函数
decoratedTarget = config.decorator(decoratedTarget, ...(config.args || []));
} else if (config.type === 'method' || config.type === 'property') {
if (!config.name) {
console.warn(`Decorator config for type "${config.type}" requires a "name" property.`);
continue;
}
// 获取当前属性或方法的描述符
const originalDescriptor = Object.getOwnPropertyDescriptor(decoratedTarget.prototype, config.name);
let newDescriptor;
// 如果装饰器是工厂函数,先调用它
const decoratorFn = typeof config.decorator === 'function' && config.decorator.length === 1 && typeof config.decorator() === 'function'
? config.decorator(...(config.args || [])) // 认为是工厂函数
: config.decorator; // 否则直接是装饰器函数
// 调用装饰器函数,获取新的描述符
newDescriptor = decoratorFn(decoratedTarget.prototype, config.name, originalDescriptor);
// 如果新的描述符有效,则重新定义属性
if (newDescriptor && (newDescriptor.value !== undefined || newDescriptor.get !== undefined || newDescriptor.set !== undefined)) {
Object.defineProperty(decoratedTarget.prototype, config.name, newDescriptor);
} else {
console.warn(`Decorator for ${config.type} "${config.name}" did not return a valid descriptor.`);
}
}
}
return decoratedTarget;
}
// --- 使用 applyDecorators ---
class ReportGenerator {
constructor(name) {
this.reportName = name;
}
generatePdf(data) {
console.log(`Generating PDF report "${this.reportName}" with data:`, data);
return `PDF for ${this.reportName}`;
}
generateCsv(data) {
console.log(`Generating CSV report "${this.reportName}" with data:`, data);
return `CSV for ${this.reportName}`;
}
reportId = Math.random().toString(36).substring(7); // 实例属性
}
// 定义验证器
const isArray = (val) => Array.isArray(val);
const isNotEmptyArray = (val) => Array.isArray(val) && val.length > 0;
// 应用装饰器配置
const DecoratedReportGenerator = applyDecorators(ReportGenerator, [
{ type: 'class', decorator: logInstantiation }, // 类装饰器
{ type: 'method', name: 'generatePdf', decorator: performanceLogger }, // 方法装饰器
{ type: 'method', name: 'generateCsv', decorator: validateArgs, args: [isNotEmptyArray] }, // 带参数的方法装饰器
{ type: 'property', name: 'reportId', decorator: readOnly } // 属性装饰器
]);
console.log('n--- Testing DecoratedReportGenerator ---');
const report = new DecoratedReportGenerator('Monthly Sales'); // 触发 logInstantiation
report.generatePdf(['Item A', 'Item B']); // 触发 performanceLogger
report.generateCsv([100, 200, 300]); // 触发 validateArgs
console.log(`Report ID: ${report.reportId}`);
report.reportId = 'new-id'; // 尝试修改只读属性
console.log(`Report ID after attempt to change: ${report.reportId}`);
try {
report.generateCsv([]); // 触发 validateArgs 错误
} catch (e) {
console.error(e.message);
}
输出示例:
[Decorator Init] Applying logInstantiation to class: ReportGenerator
[Decorator Init] Applying performanceLogger to method: generatePdf of class: ReportGenerator
[Decorator Init] Applying validateArgs to method: generateCsv of class: ReportGenerator
[Decorator Init] Applying readOnly to property: reportId of class: ReportGenerator
--- Testing DecoratedReportGenerator ---
[Instantiation Log] Instantiating ReportGenerator with arguments: [ 'Monthly Sales' ]
[Instantiation Log] ReportGenerator instance created: ReportGenerator { reportName: 'Monthly Sales', reportId: '...' }
Generating PDF report "Monthly Sales" with data: [ 'Item A', 'Item B' ]
[Performance Log] Method "generatePdf" executed in 0.08 ms with args: [ [ 'Item A', 'Item B' ] ]
Generating CSV report "Monthly Sales" with data: [ 100, 200, 300 ]
Report ID: ...
Attempted to write to read-only property "reportId". Value will not be set.
Report ID after attempt to change: ...
Argument 0 validation error in method "generateCsv": Validation failed for argument 0 in method "generateCsv".
这个 applyDecorators 辅助函数是一个更实用的方式,尽管它仍然无法完全模拟 @ 语法糖的编译时行为(特别是对于类字段的初始化顺序)。但它展示了如何将我们手写的装饰器逻辑组织起来,并在运行时应用到目标对象上。
高级考量与注意事项
this 上下文的正确处理
Proxy 陷阱中的 this 默认指向 handler 对象,而不是目标对象。Reflect 方法(如 Reflect.get, Reflect.set, Reflect.apply)的 receiver 或 thisArg 参数正是为了解决这个问题。始终使用 Reflect 转发操作,并正确传递 this 上下文,是确保代理行为符合预期的关键。
性能影响
Proxy 会引入一层间接调用。每次对被代理对象的操作都会经过 Proxy 陷阱。对于性能敏感的场景,大量使用 Proxy 可能会带来轻微的性能开销。然而,对于大多数应用程序而言,这种开销通常可以忽略不计,其带来的代码可维护性和灵活性价值远超性能损失。
装饰器组合
当一个类、方法或属性被多个装饰器装饰时,它们的执行顺序很重要。在原生提案中,装饰器通常是自下而上(从内到外)执行的。在我们的手动实现中,这意味着 applyDecorators 函数应该按照你希望的顺序来应用它们,或者在每个装饰器内部进行嵌套包装。
错误处理
在装饰器内部,特别是验证器等,应该妥善处理错误。抛出清晰的错误信息有助于调试。
实例属性 vs. 原型属性
类字段(如 myProperty = 'value';)是实例属性,它们在每个实例上独立创建。而方法通常定义在类的原型上。属性装饰器在处理这两种情况时需要不同的策略。我们上面 readOnly 和 uppercase 的例子选择在原型上定义 getter/setter 来拦截,这会覆盖同名的实例属性。更复杂的实现可能需要一个类装饰器,在 construct 陷阱中返回一个针对实例的 Proxy 来拦截实例属性。
Proxy 的局限性
- 无法直接代理私有字段和私有方法 (
#):Proxy只能拦截公共属性的访问。私有字段和方法是 JavaScript 引擎内部的实现细节,无法通过Proxy直接拦截。 - 深层嵌套对象:
Proxy默认只代理顶层对象。如果对象内部包含其他对象,这些内部对象不会自动被代理。你需要手动遍历并代理所有子对象,这会增加复杂性。 - 不完全的透明性:某些内置操作(如
instanceof)可能不会被Proxy完全欺骗。不过,在大多数常见用例中,Proxy的行为足够透明。
展望未来:原生装饰器
虽然我们今天深入探讨了如何利用 Proxy 和 Reflect 手写实现装饰器功能,但了解 JavaScript 原生装饰器提案的方向仍然很重要。原生装饰器将提供更强大的能力和更简洁的语法:
- 编译时支持:
@语法糖会在代码编译或加载时被处理,而不是完全在运行时依赖Proxy包装。 - 更细粒度的控制:提案中的装饰器接收的参数(例如
context对象)提供了关于被装饰实体更丰富的信息和操作能力,包括修改属性的kind(method, field, accessor),addInitializer等。 - 更好的性能:由于是在编译时处理,原生装饰器通常比运行时
Proxy具有更小的性能开销。
即便如此,通过 Proxy 和 Reflect 模拟实现装饰器,对于我们理解其底层机制、元编程思想以及这些强大 API 的应用,都具有不可估量的价值。
实践的意义与价值
我们今天通过 Proxy 和 Reflect 模拟的装饰器,不仅展示了如何动态地修改类、方法和属性的行为,更是对 JavaScript 元编程能力的一次深刻探索。掌握这些技术,你将能够:
- 构建更加灵活和可扩展的框架与库。
- 深入理解现代 JavaScript 生态中许多高级特性的实现原理。
- 在面对复杂需求时,拥有更多解决问题的工具和思路。
这些工具为我们打开了一扇通向更高级抽象和更强大代码控制能力的大门,让 JavaScript 不再仅仅是前端交互的脚本语言,更是能够进行深度系统构建的编程利器。