一、什么是JavaScript代理?
JavaScript中的Proxy是ES6引入的一个强大特性,它允许你创建一个对象的代理,从而拦截并自定义该对象的基本操作,如属性查找、赋值、枚举、函数调用等。Proxy为JavaScript提供了元编程能力,让开发者能够在运行时动态地拦截和修改对象行为。
Proxy的基本语法
const proxy = new Proxy(target, handler);target:要代理的目标对象(可以是任何类型的对象,包括数组、函数,甚至是另一个代理)。
handler:一个对象,包含各种拦截器方法,这些方法会在执行相应操作时被调用。
proxy:生成的代理对象,对代理对象的操作会被转发到目标对象,并通过handler中的拦截器方法进行处理。
Proxy的拦截器方法
Proxy提供了13种拦截器方法,也称为陷阱(traps),它们对应JavaScript中对象的各种基本操作:
属性访问相关
方法 | 描述 |
|---|---|
handler.get(target, property, receiver) | 拦截属性查找操作 |
handler.set(target, property, value, receiver) | 拦截属性设置操作 |
handler.deleteProperty(target, property) | 拦截属性删除操作 |
handler.has(target, property) | 拦截in操作符 |
handler.getOwnPropertyDescriptor(target, property) | 拦截Object.getOwnPropertyDescriptor() |
handler.defineProperty(target, property, descriptor) | 拦截Object.defineProperty() |
原型相关
方法 | 描述 |
|---|---|
handler.getPrototypeOf(target) | 拦截Object.getPrototypeOf() |
handler.setPrototypeOf(target, prototype) | 拦截Object.setPrototypeOf() |
枚举相关
方法 | 描述 |
|---|---|
handler.ownKeys(target) | 拦截Object.keys()、Object.getOwnPropertyNames()等 |
handler.preventExtensions(target) | 拦截Object.preventExtensions() |
handler.isExtensible(target) | 拦截Object.isExtensible() |
函数调用相关
方法 | 描述 |
|---|---|
handler.apply(target, thisArg, argumentsList) | 拦截函数调用操作 |
handler.construct(target, argumentsList, newTarget) | 拦截new操作符 |
二、Proxy的核心特性与机制
1. 透明代理
当handler不包含任何拦截器时,代理会完全透明地转发所有操作到目标对象:
const target = { name: '目标对象' };
const handler = {};
const proxy = new Proxy(target, handler);
console.log(proxy.name); // 输出: 目标对象
proxy.name = '新名称';
console.log(target.name); // 输出: 新名称2. 上下文保留(receiver参数)
receiver参数确保了正确的上下文绑定,特别是在涉及继承和setter/getter时:
const parent = {
get name() {
return this._name || '默认名称';
},
set name(value) {
this._name = value;
}
};
const handler = {
get(target, property, receiver) {
console.log(`访问属性: ${property}`);
// 使用Reflect.get确保this绑定到receiver(即代理对象)
return Reflect.get(target, property, receiver);
}
};
const proxy = new Proxy(parent, handler);
const child = Object.create(proxy);
child._name = '子对象';
console.log(child.name); // 输出: 访问属性: name 和 子对象
// 如果不使用receiver,这里会返回'默认名称'3. 不可撤销性
一旦创建了Proxy,就无法撤销它,这意味着代理和目标对象之间的连接将一直存在:
const target = {};
const proxy = new Proxy(target, {});
// 无法撤销这个代理4. 代理对象的特殊性质
代理对象没有自己的属性(除非在拦截器中显式添加)。
typeof proxy 会根据目标对象的类型返回相应的结果。
instanceof 操作符会正确地穿过代理对象。
三、Proxy的高级应用示例
1. 数据验证与保护示例
function createValidator(target, validations) {
return new Proxy(target, {
set(obj, prop, value) {
// 检查是否有该属性的验证规则
if (validations.hasOwnProperty(prop)) {
const isValid = validations[prop](value);
if (!isValid) {
throw new Error(`属性 ${prop} 的值 ${value} 无效`);
}
}
// 通过验证后设置属性
return Reflect.set(obj, prop, value);
},
get(obj, prop) {
// 保护私有属性
if (typeof prop === 'string' && prop.startsWith('_')) {
throw new Error(`无法访问私有属性: ${prop}`);
}
return Reflect.get(obj, prop);
},
deleteProperty(obj, prop) {
// 防止删除某些关键属性
const protectedProps = ['id', 'createdAt'];
if (protectedProps.includes(prop)) {
throw new Error(`属性 ${prop} 无法删除`);
}
return Reflect.deleteProperty(obj, prop);
}
});
}
// 使用示例
const userSchema = {
name: value => typeof value === 'string' && value.length >= 2,
age: value => typeof value === 'number' && value >= 0 && value <= 150,
email: value => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
};
const user = createValidator({}, userSchema);
user.name = '文曲'; // 有效
user.age = 30; // 有效
// user.age = 200; // 无效,会抛出错误
// user._password = 'secret'; // 尝试访问私有属性会抛出错误2. 响应式数据系统示例
function reactive(target, callback) {
return new Proxy(target, {
set(obj, prop, value) {
const oldValue = obj[prop];
const result = Reflect.set(obj, prop, value);
// 只有当值发生变化时才触发回调
if (oldValue !== value) {
callback(prop, oldValue, value);
}
return result;
}
});
}
// 使用示例 - 创建一个简单的响应式计数器
const counter = reactive({ count: 0 }, (prop, oldValue, newValue) => {
console.log(`属性 ${prop} 从 ${oldValue} 变为 ${newValue}`);
// 这里可以更新UI等操作
updateCounterDisplay(newValue);
});
function updateCounterDisplay(value) {
console.log(`UI更新: 当前计数为 ${value}`);
}
// 使用计数器
counter.count++; // 触发回调
counter.count = 10; // 触发回调
counter.count = 10; // 不会触发回调,因为值没有变化3. 虚拟属性和计算属性示例
function withComputed(target, computedProps) {
return new Proxy(target, {
get(obj, prop) {
// 检查是否是计算属性
if (computedProps.hasOwnProperty(prop)) {
const computeFn = computedProps[prop];
return computeFn(obj);
}
return Reflect.get(obj, prop);
},
// 防止直接修改计算属性
set(obj, prop, value) {
if (computedProps.hasOwnProperty(prop)) {
throw new Error(`无法直接设置计算属性: ${prop}`);
}
return Reflect.set(obj, prop, value);
}
});
}
// 使用示例
const person = withComputed(
{ firstName: '文', lastName: '曲', age: 30 },
{
fullName: obj => `${obj.firstName}${obj.lastName}`,
birthYear: obj => new Date().getFullYear() - obj.age,
isAdult: obj => obj.age >= 18
}
);
console.log(person.fullName); // 文曲
console.log(person.birthYear); // 根据当前年份和年龄计算
console.log(person.isAdult); // true
person.firstName = '武';
console.log(person.fullName); // 武曲 (动态更新)
// person.fullName = '破军'; // 会抛出错误,无法直接设置计算属性4. 方法调用追踪与日志
function traceMethodCalls(target) {
return new Proxy(target, {
get(obj, prop) {
const value = Reflect.get(obj, prop);
// 只代理方法
if (typeof value === 'function') {
return function (...args) {
console.log(`调用方法 ${prop},参数: ${args}`);
const startTime = performance.now();
try {
const result = value.apply(this, args);
console.log(`方法 ${prop} 返回:${result}`);
return result;
} catch(error) {
console.error(`方法 ${prop} 抛出错误:`, error);
throw error;
} finally {
const endTime = performance.now();
console.log(`方法 ${prop} 执行时间: ${endTime - startTime} ms`);
}
}
}
return value;
}
});
}
// 示例:追踪 Math 对象的方法调用
const tracedMath = traceMethodCalls(Math);
console.log(tracedMath.random()); // 调用 Math.random() 方法
// 示例:追踪简单计算器对象的方法调用
const calculator = traceMethodCalls({
add(a, b) {
return a + b;
},
subtract(a, b) {
return a - b;
},
multiply(a, b) {
return a * b;
},
divide(a, b) {
if (b === 0) {
throw new Error('除数不能为零');
}
return a / b;
}
});
console.log(calculator.add(5, 3)); // 调用 add 方法
console.log(calculator.divide(10, 2)); // 调用 divide 方法
console.log(calculator.multiply(4, 5)); // 调用 multiply 方法
console.log(calculator.subtract(10, -4)); // 调用 subtract 方法
// console.log(calculator.divide(8, 0)); // 会抛出错误四、浏览器兼容性
Proxy在现代浏览器中得到了广泛支持,但在IE11及更早版本中不可用:
点击 《Proxy 浏览器兼容性》 ,查看更多的兼容信息。
五、Proxy 的主要应用场景
1. 数据绑定与响应式系统
双向数据绑定:通过拦截属性的 get/set 操作,实现数据变化时自动更新视图。
依赖追踪:记录哪些函数依赖于哪些属性,实现精确的更新(如 Vue 3 的响应式系统核心)。
2. 属性访问控制与验证
只读属性:防止对特定属性的修改。
类型检查:在设置属性时验证值的类型是否正确。
权限控制:根据不同用户或条件限制对属性的访问。
3. 虚拟属性与计算属性
动态属性:根据其他属性动态计算值(见:虚拟属性和计算属性示例)。
懒加载属性:只有在首次访问时才计算或加载值。
属性转换:在获取或设置属性时自动转换数据格式。
4. 数据代理与封装
远程代理:本地对象代理远程数据访问。
缓存代理:为开销大的操作结果提供缓存。
访问日志:记录对对象的所有访问操作。
5. 对象属性扩展
默认值处理:当访问不存在的属性时返回默认值。
属性规范化:自动处理大小写、连字符等格式转换。
防抖/节流:为频繁的属性设置操作添加防抖或节流功能。
6. 构建框架和工具
ORM 实现:映射数据库操作到对象方法。
状态管理:管理应用状态,提供可预测的状态更新。
API 适配器:统一不同 API 的接口形式。
7. 拦截器和中间件
错误处理:统一捕获对象操作中的错误。
性能监控:测量属性访问或方法调用的性能。
AOP(面向切面编程):在不修改原始代码的情况下增强功能。
8. 安全控制
沙箱隔离:限制对象的访问范围。
输入验证:过滤不安全的输入值。
访问审计:记录敏感属性的访问历史。
六、Proxy的局限性
某些内置对象的方法无法被拦截:如Date对象的某些方法。
不能代理不可配置或不可写的属性:除非在handler的set中返回true。
无法直接拦截某些操作:如访问对象的内部插槽(internal slots)。
与WeakMap/WeakSet一起使用时有限制:无法作为这些集合的键进行跟踪。
总结
JavaScript的Proxy是一个强大的元编程工具,它允许开发者在运行时拦截和自定义对象的基本操作。通过Proxy,我们可以实现数据验证、响应式系统、计算属性、方法追踪等高级功能。合理使用Proxy,可以让代码更加灵活、可维护,并能构建出更加强大和智能的应用程序架构。
Proxy与Reflect、Symbol等其他ES6特性结合使用,可以发挥出更强大的元编程能力,但在使用时也需要注意性能和兼容性问题。
彩蛋
完整示例代码,请移步Cnb 《追踪方法调用示例》