ES6 Proxy 的 13 种拦截陷阱(Traps):如何利用它实现响应式与权限拦截
欢迎各位来到今天的讲座,我们即将深入探讨 ES6 中一项强大而又巧妙的特性——Proxy。Proxy,顾名思义,是代理。它允许我们创建一个对象的代理,从而在对该对象进行各种操作(如读取属性、设置属性、调用方法等)时,插入自定义的逻辑。这就像在对象和其使用者之间设置了一个“守卫”,所有对对象的交互都必须先经过这个守卫的检查和处理。
这项元编程(meta-programming)能力为 JavaScript 带来了前所未有的灵活性,尤其在构建响应式系统、实现细粒度权限控制、数据验证、日志记录等高级场景中,展现出其核心价值。Vue 3 的响应式系统就是 Proxy 技术最著名的应用之一,它解决了 Vue 2 中 Object.defineProperty 存在的许多局限性。
我们将逐一剖析 Proxy 提供的 13 种“拦截陷阱”(Traps),理解它们各自的功能、参数,并通过具体的代码示例,演示如何在响应式数据和权限拦截两大核心场景中利用它们。
Proxy 的基本概念与结构
在深入陷阱之前,我们先回顾一下 Proxy 的基本构造:
const proxy = new Proxy(target, handler);
target:这是你想要代理的原始对象。它可以是任何类型的对象,包括函数、数组甚至是另一个 Proxy。handler:这是一个对象,它包含了各种拦截陷阱。每一个陷阱都是一个函数,当对proxy执行某个操作时,如果handler中定义了相应的陷阱,该陷阱就会被调用。
handler 对象中的方法被称为“陷阱”,因为它们“捕获”了对 target 对象执行的基本操作。当一个操作被捕获时,陷阱函数可以执行自定义逻辑,然后决定是执行原始操作、修改操作结果,还是完全阻止操作。
为了确保代码的健壮性和未来的兼容性,强烈建议在陷阱内部通过 Reflect 对象来调用 target 上的原始操作。Reflect 是一个内置对象,提供了一组与 Proxy 陷阱方法同名的静态方法,它们的作用与默认的 target 操作行为一致。使用 Reflect 有以下几个优势:
- 一致性:
Reflect方法的签名与 Proxy 陷阱的签名一致,使得代码更易读和维护。 - 默认行为:
Reflect提供了target对象的默认行为,而无需手动实现或担心this绑定问题。 - 兼容性:即使
target对象是不可扩展的或属性不可配置,Reflect也能正确地执行操作。
现在,让我们逐一探索这 13 种拦截陷阱。
1. get(target, property, receiver)
get 陷阱用于拦截对象属性的读取操作。这包括访问属性(obj.prop)、方括号访问(obj['prop'])、以及通过原型链查找属性。
-
参数:
target:被代理的原始对象。property:被访问的属性名(字符串或 Symbol)。receiver:Proxy 或继承 Proxy 的对象,在属性访问中它就是this的值。
-
返回值:可以是任何值,它将作为属性访问的结果返回。
-
应用场景:
- 响应式系统:在属性被读取时,记录当前“作用域”(例如,一个渲染函数)对该属性的依赖。
- 默认值:当访问一个不存在的属性时,返回一个预设的默认值。
- 权限拦截:根据用户角色或上下文,决定是否允许读取某个属性,或者在读取前进行数据脱敏。
- 计算属性:模拟计算属性,当访问时动态计算其值。
-
代码示例:
// 响应式系统中的依赖追踪
const activeEffect = []; // 模拟一个全局的活跃副作用函数栈
function track(target, key) {
if (activeEffect.length > 0) {
const effect = activeEffect[activeEffect.length - 1];
console.log(`[Reactive] Tracking dependency: ${key} for effect.`);
// 实际场景中,这里会将effect与target[key]建立映射
// 例如:depsMap.get(target).get(key).add(effect);
}
}
const reactiveHandler = {
get(target, property, receiver) {
track(target, property); // 追踪依赖
return Reflect.get(target, property, receiver);
}
};
let user = { name: "Alice", age: 30 };
const reactiveUser = new Proxy(user, reactiveHandler);
// 权限拦截与默认值
const permissionHandler = {
get(target, property, receiver) {
// 模拟当前用户角色
const currentUserRole = "guest"; // 可以是 'admin', 'user', 'guest'
if (property === 'salary' && currentUserRole !== 'admin') {
console.warn(`[Permission] Access denied to 'salary' for role: ${currentUserRole}`);
return undefined; // 或者抛出错误
}
if (property === 'email' && !Reflect.has(target, property)) {
console.log(`[Default] Providing default email for missing property.`);
return '[email protected]';
}
console.log(`[Access] Reading property: ${String(property)}`);
return Reflect.get(target, property, receiver);
}
};
let employee = { name: "Bob", title: "Developer", salary: 70000 };
const securedEmployee = new Proxy(employee, permissionHandler);
activeEffect.push(() => {
console.log(`User name is: ${reactiveUser.name}`);
});
reactiveUser.name; // 输出: [Reactive] Tracking dependency: name for effect.
activeEffect.pop();
console.log(securedEmployee.name); // Output: [Access] Reading property: name, Bob
console.log(securedEmployee.salary); // Output: [Permission] Access denied to 'salary' for role: guest, undefined
console.log(securedEmployee.email); // Output: [Default] Providing default email for missing property., [email protected]
2. set(target, property, value, receiver)
set 陷阱用于拦截对象属性的赋值操作。
-
参数:
target:被代理的原始对象。property:被设置的属性名。value:要设置的新值。receiver:Proxy 或继承 Proxy 的对象,在属性赋值中它就是this的值。
-
返回值:必须返回一个布尔值。如果属性设置成功,返回
true;如果失败(例如,因为验证不通过),返回false。在严格模式下,返回false会抛出TypeError。 -
应用场景:
- 响应式系统:在属性被修改时,触发依赖于该属性的所有“作用域”进行更新。
- 数据验证:在赋值前检查值的有效性(类型、范围等),如果无效则阻止赋值或抛出错误。
- 数据转换:在赋值前对值进行格式化或转换。
- 权限拦截:根据用户角色或上下文,决定是否允许修改某个属性。
- 日志记录:记录属性的变更历史。
-
代码示例:
// 响应式系统中的触发更新
const effectsMap = new WeakMap();
function trigger(target, key) {
if (effectsMap.has(target) && effectsMap.get(target).has(key)) {
console.log(`[Reactive] Triggering effects for key: ${key}`);
// 实际场景中,这里会遍历并执行所有依赖于target[key]的effect
// effectsMap.get(target).get(key).forEach(effect => effect());
}
}
const reactiveSetHandler = {
set(target, property, value, receiver) {
const oldValue = Reflect.get(target, property, receiver);
if (oldValue === value) { // 值未改变,不触发更新
return true;
}
console.log(`[Reactive] Setting property: ${String(property)} to ${value}`);
const result = Reflect.set(target, property, value, receiver);
if (result) {
trigger(target, property); // 触发更新
}
return result;
}
};
let data = { count: 0 };
const reactiveData = new Proxy(data, reactiveSetHandler);
// 模拟注册effect
if (!effectsMap.has(data)) effectsMap.set(data, new Map());
if (!effectsMap.get(data).has('count')) effectsMap.get(data).set('count', new Set());
effectsMap.get(data).get('count').add(() => console.log('Count changed!'));
reactiveData.count = 1; // Output: [Reactive] Setting property: count to 1, [Reactive] Triggering effects for key: count, Count changed!
reactiveData.count = 1; // No output for trigger, as value is same.
reactiveData.count = 2; // Output: [Reactive] Setting property: count to 2, [Reactive] Triggering effects for key: count, Count changed!
// 权限拦截与数据验证
const validationAndPermissionHandler = {
set(target, property, value, receiver) {
const currentUserRole = "user"; // 模拟当前用户角色
if (property === 'price') {
if (currentUserRole !== 'admin') {
console.error(`[Permission] User role '${currentUserRole}' cannot change 'price'.`);
return false; // 阻止赋值
}
if (typeof value !== 'number' || value <= 0) {
console.error(`[Validation] Price must be a positive number.`);
return false; // 阻止赋值
}
}
if (property === 'name' && typeof value !== 'string') {
console.error(`[Validation] Name must be a string.`);
return false;
}
console.log(`[Set] Setting property ${String(property)} to ${value}`);
return Reflect.set(target, property, value, receiver);
}
};
let product = { name: "Laptop", price: 1200 };
const securedProduct = new Proxy(product, validationAndPermissionHandler);
securedProduct.name = "Gaming Laptop"; // Output: [Set] Setting property name to Gaming Laptop
console.log(securedProduct.name); // Output: Gaming Laptop
securedProduct.price = 1500; // Output: [Permission] User role 'user' cannot change 'price'. (returns false)
console.log(securedProduct.price); // Output: 1200 (unchanged)
// 切换到 admin 角色,并尝试无效值
// const currentUserRole = "admin"; // 假设角色切换
// securedProduct.price = "invalid"; // Output: [Validation] Price must be a positive number.
// securedProduct.price = -100; // Output: [Validation] Price must be a positive number.
// securedProduct.price = 1500; // Output: [Set] Setting property price to 1500
// console.log(securedProduct.price); // Output: 1500
3. has(target, property)
has 陷阱用于拦截 in 操作符。
-
参数:
target:被代理的原始对象。property:要检查的属性名。
-
返回值:必须返回一个布尔值。
-
应用场景:
- 隐藏私有属性:让某些属性在
in检查时表现为不存在。 - 条件属性存在:根据特定条件决定属性是否“存在”。
- 权限拦截:控制用户是否能发现某个属性的存在。
- 隐藏私有属性:让某些属性在
-
代码示例:
const hiddenPropertyHandler = {
has(target, property) {
if (property.startsWith('_')) {
console.log(`[Has] Hiding private property: ${String(property)}`);
return false; // 隐藏以 '_' 开头的属性
}
console.log(`[Has] Checking property: ${String(property)}`);
return Reflect.has(target, property);
}
};
let config = { id: 1, name: "AppConfig", _secret: "super_secret" };
const securedConfig = new Proxy(config, hiddenPropertyHandler);
console.log('id' in securedConfig); // Output: [Has] Checking property: id, true
console.log('_secret' in securedConfig); // Output: [Has] Hiding private property: _secret, false
console.log('version' in securedConfig); // Output: [Has] Checking property: version, false
4. deleteProperty(target, property)
deleteProperty 陷阱用于拦截 delete 操作符。
-
参数:
target:被代理的原始对象。property:要删除的属性名。
-
返回值:必须返回一个布尔值。如果属性删除成功,返回
true;如果失败,返回false。在严格模式下,返回false会抛出TypeError。 -
应用场景:
- 防止删除:阻止某些关键属性被删除。
- 权限拦截:根据用户角色决定是否允许删除属性。
- 日志记录:记录属性的删除操作。
- 级联删除:当删除某个属性时,自动删除与之相关的其他属性。
-
代码示例:
const deleteGuardHandler = {
deleteProperty(target, property) {
if (property === 'id') {
console.warn(`[Delete] Cannot delete 'id' property.`);
return false; // 阻止删除
}
console.log(`[Delete] Deleting property: ${String(property)}`);
return Reflect.deleteProperty(target, property);
}
};
let item = { id: 101, name: "Widget", description: "A useful tool" };
const securedItem = new Proxy(item, deleteGuardHandler);
console.log(delete securedItem.id); // Output: [Delete] Cannot delete 'id' property., false
console.log(item); // Output: { id: 101, name: 'Widget', description: 'A useful tool' } (id unchanged)
console.log(delete securedItem.description); // Output: [Delete] Deleting property: description, true
console.log(item); // Output: { id: 101, name: 'Widget' }
5. apply(target, thisArg, argumentsList)
apply 陷阱用于拦截函数的调用。target 必须是一个函数。
-
参数:
target:被代理的函数。thisArg:调用函数时传入的this值。argumentsList:调用函数时传入的参数列表(数组)。
-
返回值:可以是任何值,作为函数调用的结果返回。
-
应用场景:
- 函数调用日志:记录函数的调用,包括参数和返回值。
- 参数验证:在函数执行前验证参数的有效性。
- 访问控制:根据权限决定是否允许执行函数。
- 函数节流/防抖:在函数执行前后插入节流或防抖逻辑。
- 错误处理:统一捕获函数执行中的错误。
-
代码示例:
const functionInterceptorHandler = {
apply(target, thisArg, argumentsList) {
console.log(`[Apply] Calling function: ${target.name || 'anonymous'} with args: ${JSON.stringify(argumentsList)}`);
// 模拟权限检查
const hasPermission = argumentsList.length > 0 && argumentsList[0] === 'admin_token';
if (!hasPermission && target.name === 'deleteRecord') {
console.error(`[Permission] Unauthorized attempt to call ${target.name}.`);
return `Error: Unauthorized.`;
}
const result = Reflect.apply(target, thisArg, argumentsList);
console.log(`[Apply] Function ${target.name || 'anonymous'} returned: ${result}`);
return result;
}
};
function sum(a, b) {
return a + b;
}
function deleteRecord(id) {
// 假设这里执行删除操作
return `Record ${id} deleted successfully.`;
}
const interceptedSum = new Proxy(sum, functionInterceptorHandler);
const interceptedDelete = new Proxy(deleteRecord, functionInterceptorHandler);
console.log(interceptedSum(5, 3)); // Output: [Apply] Calling..., [Apply] Function... returned..., 8
console.log(interceptedDelete(123)); // Output: [Apply] Calling..., [Permission] Unauthorized..., Error: Unauthorized.
console.log(interceptedDelete('admin_token', 456)); // Output: [Apply] Calling..., [Apply] Function... returned..., Record 456 deleted successfully.
6. construct(target, argumentsList, newTarget)
construct 陷阱用于拦截 new 操作符。target 必须是一个构造函数。
-
参数:
target:被代理的构造函数。argumentsList:new操作符传入的参数列表。newTarget:最初被调用的构造函数,通常是 Proxy 本身。
-
返回值:必须返回一个对象。
-
应用场景:
- 构造函数日志:记录实例的创建。
- 参数验证:在创建实例前验证构造函数参数。
- 自定义实例化:返回一个完全不同的对象实例,例如实现单例模式。
-
代码示例:
const constructorInterceptorHandler = {
construct(target, argumentsList, newTarget) {
console.log(`[Construct] Creating new instance of ${target.name || 'anonymous'} with args: ${JSON.stringify(argumentsList)}`);
if (target.name === 'User' && argumentsList[0] === null) {
console.error(`[Validation] User name cannot be null.`);
throw new Error('Invalid user name.');
}
// 确保使用 Reflect.construct 保持正确的原型链
const instance = Reflect.construct(target, argumentsList, newTarget);
console.log(`[Construct] Instance created:`, instance);
instance.createdAt = new Date(); // 为所有新实例添加创建时间
return instance;
}
};
class User {
constructor(name, age) {
this.name = name;
this.age = age;
}
}
const InterceptedUser = new Proxy(User, constructorInterceptorHandler);
const user1 = new InterceptedUser("Alice", 30);
console.log(user1);
/* Output:
[Construct] Creating new instance of User with args: ["Alice",30]
[Construct] Instance created: User { name: 'Alice', age: 30 }
User { name: 'Alice', age: 30, createdAt: 2023-10-27T...Z }
*/
try {
new InterceptedUser(null, 25); // Output: [Construct] Creating..., [Validation] User name cannot be null., Error: Invalid user name.
} catch (e) {
console.error(e.message);
}
7. getOwnPropertyDescriptor(target, property)
getOwnPropertyDescriptor 陷阱用于拦截 Object.getOwnPropertyDescriptor()。
-
参数:
target:被代理的原始对象。property:要获取描述符的属性名。
-
返回值:必须返回一个属性描述符对象(如果属性存在)或
undefined(如果属性不存在)。如果返回的描述符与真实描述符不一致,可能会导致TypeError。 -
应用场景:
- 修改属性描述符:动态改变属性的
writable,enumerable,configurable状态。 - 隐藏属性:通过返回
undefined来让属性看起来不存在。
- 修改属性描述符:动态改变属性的
-
代码示例:
const descriptorModifierHandler = {
getOwnPropertyDescriptor(target, property) {
console.log(`[Descriptor] Getting descriptor for: ${String(property)}`);
const descriptor = Reflect.getOwnPropertyDescriptor(target, property);
if (descriptor && property === 'secretData') {
console.log(`[Descriptor] Modifying 'secretData' descriptor: enumerable=false`);
descriptor.enumerable = false; // 隐藏属性,使其不可枚举
}
return descriptor;
}
};
let sensitiveInfo = { id: 1, name: "Project X", secretData: "top_secret" };
const securedInfo = new Proxy(sensitiveInfo, descriptorModifierHandler);
const desc = Object.getOwnPropertyDescriptor(securedInfo, 'secretData');
console.log(desc);
/* Output:
[Descriptor] Getting descriptor for: secretData
[Descriptor] Modifying 'secretData' descriptor: enumerable=false
{ value: 'top_secret',
writable: true,
enumerable: false, // 已经被修改
configurable: true }
*/
for (let key in securedInfo) {
console.log(`[For...in] ${key}: ${securedInfo[key]}`);
}
// Output: [For...in] id: 1, [For...in] name: Project X (secretData 未被枚举)
8. defineProperty(target, property, descriptor)
defineProperty 陷阱用于拦截 Object.defineProperty()、Object.defineProperties()。
-
参数:
target:被代理的原始对象。property:要定义的属性名。descriptor:属性描述符对象。
-
返回值:必须返回一个布尔值。如果属性定义成功,返回
true;如果失败,返回false。在严格模式下,返回false会抛出TypeError。 -
应用场景:
- 防止属性添加/修改:阻止新的属性被定义,或者阻止现有属性的描述符被修改。
- 权限拦截:根据用户角色决定是否允许定义或修改属性。
- 强制属性规则:确保所有新定义的属性都符合特定规范(例如,都是不可配置的)。
- 日志记录:记录属性的定义操作。
-
代码示例:
const defineGuardHandler = {
defineProperty(target, property, descriptor) {
console.log(`[DefineProperty] Attempting to define property: ${String(property)}`);
if (property === 'version') {
console.warn(`[DefineProperty] Cannot define 'version' property.`);
return false; // 阻止定义 'version' 属性
}
if (descriptor.configurable === false) {
console.warn(`[DefineProperty] Forcing 'configurable' to true for ${String(property)}.`);
descriptor.configurable = true; // 强制设置为可配置
}
return Reflect.defineProperty(target, property, descriptor);
}
};
let appSettings = { theme: 'dark' };
const securedAppSettings = new Proxy(appSettings, defineGuardHandler);
Object.defineProperty(securedAppSettings, 'version', {
value: '1.0.0',
writable: false
});
// Output: [DefineProperty] Attempting to define property: version, [DefineProperty] Cannot define 'version' property.
console.log(appSettings); // Output: { theme: 'dark' } (version 未被添加)
Object.defineProperty(securedAppSettings, 'language', {
value: 'en',
writable: true,
configurable: false // 尝试设置为不可配置
});
/* Output:
[DefineProperty] Attempting to define property: language
[DefineProperty] Forcing 'configurable' to true for language.
*/
const desc = Object.getOwnPropertyDescriptor(appSettings, 'language');
console.log(desc.configurable); // Output: true (被强制为 true)
9. getPrototypeOf(target)
getPrototypeOf 陷阱用于拦截 Object.getPrototypeOf()、Reflect.getPrototypeOf()、instanceof 检查以及 __proto__ 属性的读取。
-
参数:
target:被代理的原始对象。
-
返回值:必须返回一个对象或
null。 -
应用场景:
- 伪造原型链:改变对象在
instanceof检查中的行为。 - 隐藏原型:阻止外部代码发现真实的原型链。
- 伪造原型链:改变对象在
-
代码示例:
const prototypeModifierHandler = {
getPrototypeOf(target) {
console.log(`[GetPrototypeOf] Getting prototype of`, target);
// 假设我们想让所有代理对象看起来都继承自一个特定的“虚拟”原型
if (target.type === 'virtual') {
return {
isVirtual: true,
getVirtualName() { return "VirtualObject"; }
};
}
return Reflect.getPrototypeOf(target);
}
};
let realObject = {};
let virtualObject = { type: 'virtual', data: 123 };
const realProxy = new Proxy(realObject, prototypeModifierHandler);
const virtualProxy = new Proxy(virtualObject, prototypeModifierHandler);
console.log(Object.getPrototypeOf(realProxy)); // Output: [GetPrototypeOf] Getting prototype of {}, {}
console.log(Object.getPrototypeOf(virtualProxy)); // Output: [GetPrototypeOf] Getting prototype of { type: 'virtual', data: 123 }, { isVirtual: true, getVirtualName: [Function: getVirtualName] }
10. setPrototypeOf(target, prototype)
setPrototypeOf 陷阱用于拦截 Object.setPrototypeOf()。
-
参数:
target:被代理的原始对象。prototype:要设置的新原型对象(或null)。
-
返回值:必须返回一个布尔值。如果原型设置成功,返回
true;如果失败,返回false。 -
应用场景:
- 防止原型链修改:阻止外部代码改变对象的原型。
- 日志记录:记录原型链的变更尝试。
-
代码示例:
const preventSetPrototypeHandler = {
setPrototypeOf(target, prototype) {
console.log(`[SetPrototypeOf] Attempting to set prototype of`, target, `to`, prototype);
if (target.isLocked) {
console.warn(`[SetPrototypeOf] Object is locked, prototype cannot be changed.`);
return false; // 阻止修改原型
}
return Reflect.setPrototypeOf(target, prototype);
}
};
let mutableObject = { a: 1 };
let lockedObject = { b: 2, isLocked: true };
const mutableProxy = new Proxy(mutableObject, preventSetPrototypeHandler);
const lockedProxy = new Proxy(lockedObject, preventSetPrototypeHandler);
const newProto = { c: 3 };
console.log(Object.setPrototypeOf(mutableProxy, newProto)); // Output: [SetPrototypeOf] Attempting..., true
console.log(Object.getPrototypeOf(mutableProxy)); // Output: { c: 3 }
console.log(Object.setPrototypeOf(lockedProxy, newProto)); // Output: [SetPrototypeOf] Attempting..., [SetPrototypeOf] Object is locked..., false
console.log(Object.getPrototypeOf(lockedProxy)); // Output: {} (原型未变)
11. enumerate(target) (已废弃,推荐使用 ownKeys)
enumerate 陷阱在 ES6 规范中曾用于拦截 for...in 循环,但它已被 ownKeys 陷阱取代,并且不再是标准的一部分。虽然一些旧的 JavaScript 环境可能仍然支持它,但在现代代码中应避免使用。为了完整性,这里简要提及。
-
参数:
target:被代理的原始对象。
-
返回值:必须返回一个迭代器对象。
-
代码示例 (仅作示意,不推荐在生产环境使用):
// const enumerateHandler = {
// enumerate(target) {
// console.log(`[Enumerate] Intercepting for...in enumeration.`);
// return Reflect.enumerate(target); // 通常直接转发
// }
// };
//
// let myObject = { a: 1, b: 2 };
// const myProxy = new Proxy(myObject, enumerateHandler);
//
// for (let key in myProxy) {
// console.log(key);
// }
12. ownKeys(target)
ownKeys 陷阱用于拦截 Object.keys()、Object.getOwnPropertyNames()、Object.getOwnPropertySymbols()、for...in 循环以及 JSON.stringify()。它是 enumerate 的现代替代品,功能更强大。
-
参数:
target:被代理的原始对象。
-
返回值:必须返回一个由字符串或 Symbol 组成的数组。
-
应用场景:
- 隐藏私有属性:让某些属性不出现在属性列表中。
- 动态属性列表:根据条件改变可枚举属性的列表。
- 权限拦截:控制用户能看到哪些属性。
-
代码示例:
const ownKeysHandler = {
ownKeys(target) {
console.log(`[OwnKeys] Intercepting ownKeys operation for`, target);
const keys = Reflect.ownKeys(target);
// 过滤掉以 '_' 开头的私有属性
const filteredKeys = keys.filter(key => !String(key).startsWith('_'));
console.log(`[OwnKeys] Returning filtered keys: ${JSON.stringify(filteredKeys)}`);
return filteredKeys;
}
};
let dataObject = { publicA: 1, publicB: 2, _privateC: 3, [Symbol('privateD')]: 4 };
const securedDataObject = new Proxy(dataObject, ownKeysHandler);
console.log(Object.keys(securedDataObject)); // Output: [OwnKeys]..., [OwnKeys]..., ["publicA", "publicB"]
console.log(Object.getOwnPropertyNames(securedDataObject)); // Output: [OwnKeys]..., [OwnKeys]..., ["publicA", "publicB"]
console.log(Object.getOwnPropertySymbols(securedDataObject)); // Output: [OwnKeys]..., [OwnKeys]..., [] (Symbol 也被过滤了,因为startsWith('_')对Symbol转字符串后不符合)
// for...in 也受 ownKeys 影响
for (let key in securedDataObject) {
console.log(`[For...in] ${key}: ${securedDataObject[key]}`);
}
// Output: [For...in] publicA: 1, [For...in] publicB: 2
13. isExtensible(target)
isExtensible 陷阱用于拦截 Object.isExtensible()。
-
参数:
target:被代理的原始对象。
-
返回值:必须返回一个布尔值。
-
应用场景:
- 伪造可扩展性:让对象看起来不可扩展,即使它实际上可以添加新属性。
-
代码示例:
const extensibleHandler = {
isExtensible(target) {
console.log(`[IsExtensible] Checking extensibility of`, target);
// 假设我们想让所有对象看起来都不可扩展
return false;
}
};
let myPlainObject = {};
const nonExtensibleProxy = new Proxy(myPlainObject, extensibleHandler);
console.log(Object.isExtensible(myPlainObject)); // Output: true (原始对象可扩展)
console.log(Object.isExtensible(nonExtensibleProxy)); // Output: [IsExtensible]..., false (代理对象看起来不可扩展)
// 尝试向代理添加新属性,会因为代理说不可扩展而在严格模式下报错
try {
nonExtensibleProxy.newProp = "test";
} catch (e) {
console.error(e.message); // Output: TypeError: 'set' on proxy: property 'newProp' cannot be added to non-extensible object
}
14. preventExtensions(target)
preventExtensions 陷阱用于拦截 Object.preventExtensions()。
-
参数:
target:被代理的原始对象。
-
返回值:必须返回一个布尔值。如果操作成功,返回
true;如果失败,返回false。 -
应用场景:
- 强制不可扩展:在调用
Object.preventExtensions()时执行额外逻辑。 - 日志记录:记录对象被设置为不可扩展的操作。
- 强制不可扩展:在调用
-
代码示例:
const preventExtensionLoggerHandler = {
preventExtensions(target) {
console.log(`[PreventExtensions] Attempting to prevent extensions on`, target);
// 可以添加额外的检查或清理逻辑
if (target.canBeExtended === false) {
console.warn(`[PreventExtensions] Target already cannot be extended.`);
return false; // 假设 target 自身有一些不可扩展的标志
}
const result = Reflect.preventExtensions(target);
if (result) {
console.log(`[PreventExtensions] Successfully prevented extensions.`);
} else {
console.error(`[PreventExtensions] Failed to prevent extensions.`);
}
return result;
}
};
let dataContainer = { value: 100, canBeExtended: true };
const preventExtensibleProxy = new Proxy(dataContainer, preventExtensionLoggerHandler);
console.log(Object.isExtensible(preventExtensibleProxy)); // Output: true (默认可扩展)
Object.preventExtensions(preventExtensibleProxy); // Output: [PreventExtensions]..., [PreventExtensions]...Successfully...
console.log(Object.isExtensible(preventExtensibleProxy)); // Output: false (现在不可扩展)
let rigidContainer = { value: 200, canBeExtended: false };
const rigidProxy = new Proxy(rigidContainer, preventExtensionLoggerHandler);
console.log(Object.preventExtensions(rigidProxy)); // Output: [PreventExtensions]..., [PreventExtensions]...Target already cannot be extended., false
结合陷阱实现响应式与权限拦截
理解了单个陷阱的功能后,我们可以将它们组合起来,构建更复杂的系统。
响应式数据系统(类 Vue 3)
一个简化的响应式系统需要 get 来追踪依赖,set 来触发更新,以及 deleteProperty 来处理属性删除。
核心思路:
- 依赖收集 (Dependency Collection):当一个响应式属性被
get访问时,记录下当前正在运行的“副作用”(effect,例如一个组件的渲染函数)与该属性的关联。 - 派发更新 (Trigger Update):当一个响应式属性被
set修改时,找到所有依赖于该属性的副作用,并执行它们。 - 处理删除:当属性被删除时,也需要触发相关依赖的更新。
// 模拟一个全局的活跃副作用函数栈
const effectStack = [];
// WeakMap: target -> Map<key, Set<effects>>
const targetMap = new WeakMap();
// 依赖收集
function track(target, key) {
const effect = effectStack[effectStack.length - 1];
if (effect) {
let depsMap = targetMap.get(target);
if (!depsMap) {
depsMap = new Map();
targetMap.set(target, depsMap);
}
let dep = depsMap.get(key);
if (!dep) {
dep = new Set();
depsMap.set(key, dep);
}
dep.add(effect);
console.log(`[Reactive] Tracking effect for ${String(key)}.`);
}
}
// 派发更新
function trigger(target, key) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const dep = depsMap.get(key);
if (dep) {
console.log(`[Reactive] Triggering effects for ${String(key)}.`);
dep.forEach(effect => effect());
}
}
// 创建响应式对象
function reactive(obj) {
return new Proxy(obj, {
get(target, key, receiver) {
track(target, key); // 依赖收集
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
const oldValue = Reflect.get(target, key, receiver);
const result = Reflect.set(target, key, value, receiver);
if (oldValue !== value) { // 值发生变化才触发
trigger(target, key); // 派发更新
}
return result;
},
deleteProperty(target, key) {
const hasKey = Reflect.has(target, key);
const result = Reflect.deleteProperty(target, key);
if (hasKey && result) { // 只有成功删除了已存在的属性才触发
trigger(target, key); // 派发更新
}
return result;
},
// 确保for...in, Object.keys等也能触发依赖
has(target, key) {
track(target, key);
return Reflect.has(target, key);
},
ownKeys(target) {
// 对于数组,这里可以追踪length属性
track(target, 'length'); // 简化处理,所有迭代都追踪length
return Reflect.ownKeys(target);
}
});
}
// 模拟一个副作用函数
function effect(fn) {
effectStack.push(fn); // 将当前副作用推入栈中
fn(); // 立即执行一次,进行依赖收集
effectStack.pop(); // 执行完毕后弹出
}
// 示例
const state = reactive({ count: 0, message: "Hello", list: [1, 2] });
console.log("--- Initial effects ---");
effect(() => {
console.log(`Effect 1: Count is ${state.count}`);
});
effect(() => {
console.log(`Effect 2: Message is "${state.message}"`);
});
effect(() => {
console.log(`Effect 3: List length is ${state.list.length}`);
});
console.log("n--- Modifying count ---");
state.count++; // Output: [Reactive] Setting..., [Reactive] Triggering..., Effect 1: Count is 1
console.log("n--- Modifying message ---");
state.message = "World"; // Output: [Reactive] Setting..., [Reactive] Triggering..., Effect 2: Message is "World"
console.log("n--- Adding to list ---");
state.list.push(3); // Output: [Reactive] Setting..., [Reactive] Triggering..., Effect 3: List length is 3 (由于数组操作会改变length)
console.log("n--- Deleting property ---");
delete state.message; // Output: [Reactive] Deleting..., [Reactive] Triggering..., Effect 2: Message is "undefined"
console.log("n--- Accessing non-existent property ---");
console.log('nonExistent' in state); // Output: [Reactive] Tracking..., false
细粒度权限拦截
通过组合 get, set, deleteProperty, apply, construct 等,我们可以实现非常精细的权限控制。
const roles = {
ADMIN: ['read', 'write', 'delete', 'execute'],
EDITOR: ['read', 'write'],
VIEWER: ['read']
};
function createSecureProxy(target, userRole) {
const userPermissions = roles[userRole] || [];
const checkPermission = (operation, property = null) => {
if (!userPermissions.includes(operation)) {
const msg = property ?
`User role '${userRole}' is not allowed to ${operation} property '${String(property)}'.` :
`User role '${userRole}' is not allowed to ${operation} this resource.`;
console.warn(`[Security] ${msg}`);
return false;
}
return true;
};
return new Proxy(target, {
get(target, property, receiver) {
if (!checkPermission('read', property)) {
return undefined; // 或者抛出错误
}
return Reflect.get(target, property, receiver);
},
set(target, property, value, receiver) {
if (!checkPermission('write', property)) {
return false;
}
return Reflect.set(target, property, value, receiver);
},
deleteProperty(target, property) {
if (!checkPermission('delete', property)) {
return false;
}
return Reflect.deleteProperty(target, property);
},
apply(target, thisArg, argumentsList) {
if (typeof target === 'function' && !checkPermission('execute')) {
return () => console.warn(`[Security] User role '${userRole}' is not allowed to execute this function.`);
}
return Reflect.apply(target, thisArg, argumentsList);
},
construct(target, argumentsList, newTarget) {
if (typeof target === 'function' && !checkPermission('execute')) {
throw new Error(`[Security] User role '${userRole}' is not allowed to construct new instances.`);
}
return Reflect.construct(target, argumentsList, newTarget);
},
// 隐藏不具备读权限的属性
has(target, property) {
if (!checkPermission('read', property)) {
return false;
}
return Reflect.has(target, property);
},
ownKeys(target) {
const allKeys = Reflect.ownKeys(target);
return allKeys.filter(key => checkPermission('read', key));
}
});
}
// 示例数据和函数
const projectData = {
id: 'P101',
name: 'Secret Project',
budget: 1000000,
status: 'In Progress',
_internalNotes: 'Highly confidential details'
};
class TaskManager {
constructor(project) { this.project = project; }
addTask(taskName) { console.log(`Adding task "${taskName}" to ${this.project.name}`); return true; }
deleteTask(taskId) { console.log(`Deleting task ${taskId} from ${this.project.name}`); return true; }
}
const taskManagerInstance = new TaskManager(projectData);
// 创建不同角色的代理
const adminProject = createSecureProxy(projectData, 'ADMIN');
const editorProject = createSecureProxy(projectData, 'EDITOR');
const viewerProject = createSecureProxy(projectData, 'VIEWER');
const adminTaskManager = createSecureProxy(taskManagerInstance.addTask.bind(taskManagerInstance), 'ADMIN');
const editorTaskManager = createSecureProxy(taskManagerInstance.addTask.bind(taskManagerInstance), 'EDITOR');
const viewerTaskManager = createSecureProxy(taskManagerInstance.addTask.bind(taskManagerInstance), 'VIEWER');
console.log("--- Admin Access ---");
console.log(adminProject.name); // Output: Secret Project
adminProject.budget = 1200000; // Output: (no warn)
console.log(adminProject.budget); // Output: 1200000
delete adminProject._internalNotes; // Output: (no warn)
console.log(adminTaskManager("Admin task")); // Output: Adding task "Admin task"...
console.log("n--- Editor Access ---");
console.log(editorProject.name); // Output: Secret Project
editorProject.status = 'Completed'; // Output: (no warn)
console.log(editorProject.status); // Output: Completed
editorProject.budget = 1500000; // Output: [Security] User role 'EDITOR' is not allowed to write property 'budget'.
console.log(editorProject.budget); // Output: 1200000 (unchanged)
delete editorProject._internalNotes; // Output: [Security] User role 'EDITOR' is not allowed to delete property '_internalNotes'.
console.log(editorTaskManager("Editor task")); // Output: Adding task "Editor task"...
console.log("n--- Viewer Access ---");
console.log(viewerProject.name); // Output: Secret Project
console.log(viewerProject.budget); // Output: 1200000
viewerProject.status = 'Archived'; // Output: [Security] User role 'VIEWER' is not allowed to write property 'status'.
console.log(viewerProject.status); // Output: Completed (unchanged)
console.log(viewerProject._internalNotes); // Output: [Security] User role 'VIEWER' is not allowed to read property '_internalNotes'., undefined
console.log('budget' in viewerProject); // Output: true
console.log('_internalNotes' in viewerProject); // Output: [Security] User role 'VIEWER' is not allowed to read property '_internalNotes'., false
console.log(Object.keys(viewerProject)); // Output: [Security] User role 'VIEWER' is not allowed to read property '_internalNotes'., [ 'id', 'name', 'budget', 'status' ]
viewerTaskManager("Viewer task"); // Output: [Security] User role 'VIEWER' is not allowed to execute this function.
性能考量与最佳实践
Proxy 带来了极大的灵活性,但并非没有代价。每次对代理对象的操作都会经过陷阱函数的处理,这会引入一定的性能开销。
- 性能开销:与直接操作原始对象相比,Proxy 肯定会慢一些。对于性能要求极高的场景,需要仔细权衡。然而,对于大多数应用程序而言,Proxy 带来的抽象和功能优势通常远大于其微小的性能损失。
- Reflect 的重要性:始终使用
ReflectAPI 在陷阱中执行默认操作。这不仅能保证行为的正确性,还能提高代码的健壮性和可读性。直接操作target可能会在某些情况下导致this上下文问题或意外行为。 - Revocable Proxies (可撤销代理):
Proxy.revocable(target, handler)可以创建一个可撤销的代理。一旦代理被撤销,所有对其的后续操作都会抛出TypeError。这在处理敏感对象或需要临时授权的场景中非常有用。 - Proxy 链:可以创建 Proxy 的 Proxy,形成一个拦截链。这允许你分层地添加不同的拦截逻辑。
- 避免无限递归:在
get和set陷阱中,如果你在陷阱内部再次访问代理对象,需要确保不会形成无限递归。例如,在get陷阱中直接return this[property]而不是Reflect.get(target, property, receiver)可能会导致递归。
总结
ES6 Proxy 是 JavaScript 语言中一项革命性的元编程特性,它通过 13 种拦截陷阱,提供了对对象底层操作的强大控制能力。无论是构建像 Vue 3 那样的响应式数据系统,实现细致入微的权限管理,还是进行数据验证、日志记录等,Proxy 都提供了一个优雅且高效的解决方案。掌握这些陷阱,将极大地拓宽你在 JavaScript 应用开发中的设计思路和实现能力。