一、Proxy 的核心概念
定义与本质
Proxy(代理)是 ES6 引入的元编程特性,用于创建对象的代理,从而拦截并自定义对象的基本操作。这些操作包括属性访问、赋值、枚举、函数调用等。
元编程的意义
-
元编程是指程序能够编写、操作其他程序(或自身)的编程范式
-
Proxy 允许在语言层面修改默认行为,无需修改源代码
-
优势:提高开发效率,增加程序灵活度,实现动态行为控制
基本语法
ini
const proxy = new Proxy(target, handler);
-
target:要包装的目标对象 -
handler:包含拦截器(traps)的对象
二、核心拦截器详解
1. get() - 拦截属性读取
javascript
const handler = {
get(target, property, receiver) {
console.log(`读取属性: ${property}`);
return Reflect.get(target, property, receiver);
}
};
const proxy = new Proxy({ name: '张三', age: 25 }, handler);
console.log(proxy.name); // 输出: 读取属性: name → 张三
特殊应用 - 数组负索引访问:
ini
const createNegativeArray = (arr) => new Proxy(arr, {
get(target, index, receiver) {
const idx = parseInt(index);
if (idx < 0) {
return target[target.length + idx];
}
return Reflect.get(target, index, receiver);
}
});
const arr = createNegativeArray([1, 2, 3, 4]);
console.log(arr[-1]); // 4
console.log(arr[-2]); // 3
2. set() - 拦截属性赋值
javascript
const createValidatedObject = () => {
return new Proxy({}, {
set(target, property, value, receiver) {
// 年龄验证:必须是 0-150 之间的整数
if (property === 'age') {
if (!Number.isInteger(value) || value < 0 || value > 150) {
throw new Error('年龄必须为 0-150 之间的整数');
}
}
// 邮箱格式验证
if (property === 'email') {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(value)) {
throw new Error('邮箱格式不正确');
}
}
return Reflect.set(target, property, value, receiver);
}
});
};
const user = createValidatedObject();
user.age = 25; // 成功
user.age = 200; // 抛出错误
3. deleteProperty() - 拦截删除操作
javascript
const createProtectedObject = (obj) => {
const PROTECTED_PROPS = ['id', 'createdAt'];
return new Proxy(obj, {
deleteProperty(target, property) {
if (PROTECTED_PROPS.includes(property)) {
console.warn(`属性 "${property}" 受保护,不可删除`);
return false;
}
console.log(`删除属性: ${property}`);
return Reflect.deleteProperty(target, property);
}
});
};
const data = createProtectedObject({ id: 1, name: '张三', createdAt: '2024-01-01' });
delete data.name; // 成功
delete data.id; // 警告:属性 "id" 受保护,不可删除
4. apply() - 拦截函数调用
javascript
const createLoggingFunction = (fn) => {
return new Proxy(fn, {
apply(target, thisArg, argumentsList) {
console.log(`调用函数: ${target.name}`);
console.log('参数:', argumentsList);
const start = performance.now();
const result = Reflect.apply(target, thisArg, argumentsList);
const end = performance.now();
console.log(`执行时间: ${end - start}ms`);
return result;
}
});
};
const calculateSum = createLoggingFunction((a, b) => a + b);
calculateSum(5, 3);
// 输出:
// 调用函数: calculateSum
// 参数: [5, 3]
// 执行时间: 0.05ms
// 返回: 8
5. 其他常用拦截器
-
construct():拦截new操作符 -
has():拦截in操作符 -
ownKeys():拦截Object.getOwnPropertyNames()等 -
getPrototypeOf():拦截Object.getPrototypeOf()
三、Reflect API 的配合使用
Reflect 提供了与 Proxy 拦截器一一对应的静态方法,用于执行默认操作:
javascript
const proxy = new Proxy({}, {
get(target, property, receiver) {
console.log(`访问属性: ${property}`);
// 使用 Reflect 执行默认的 get 操作
return Reflect.get(target, property, receiver);
},
set(target, property, value, receiver) {
console.log(`设置属性: ${property} = ${value}`);
// 使用 Reflect 执行默认的 set 操作
return Reflect.set(target, property, value, receiver);
}
});
四、实际应用场景
1. 数据验证与格式化
javascript
const createValidatedForm = () => {
return new Proxy({}, {
set(target, property, value) {
// 电话号码格式化和验证
if (property === 'phone') {
const cleaned = value.replace(/\D/g, '');
if (cleaned.length !== 11) {
throw new Error('电话号码必须为11位数字');
}
value = cleaned.replace(/(\d{3})(\d{4})(\d{4})/, '$1-$2-$3');
}
// 日期格式化
if (property === 'birthday' && value instanceof Date) {
value = value.toISOString().split('T')[0];
}
return Reflect.set(target, property, value);
}
});
};
2. 私有属性保护
javascript
const createPrivateAPI = () => {
const privateData = {
_apiKey: 'secret-key-12345',
_tokens: new Set()
};
const PUBLIC_METHODS = ['fetchData', 'submitData'];
const PRIVATE_PROPS = ['_apiKey', '_tokens'];
return new Proxy(privateData, {
get(target, property) {
if (PRIVATE_PROPS.includes(property)) {
throw new Error(`无权访问私有属性: ${property}`);
}
if (PUBLIC_METHODS.includes(property)) {
return function(...args) {
console.log(`调用 ${property},使用密钥: ${privateData._apiKey.slice(0, 5)}...`);
// 实际的方法实现
};
}
return Reflect.get(target, property);
},
set(target, property, value) {
if (PRIVATE_PROPS.includes(property)) {
throw new Error(`无权修改私有属性: ${property}`);
}
return Reflect.set(target, property, value);
},
ownKeys(target) {
return Object.keys(target).filter(key => !PRIVATE_PROPS.includes(key));
}
});
};
3. 实现观察者模式
javascript
const createObservable = (initialState = {}) => {
const observers = new Set();
const observable = new Proxy(initialState, {
set(target, property, value, receiver) {
const oldValue = target[property];
const result = Reflect.set(target, property, value, receiver);
// 值发生变化时通知所有观察者
if (oldValue !== value) {
observers.forEach(observer => {
observer(property, oldValue, value);
});
}
return result;
}
});
return {
observable,
subscribe: (observer) => {
observers.add(observer);
return () => observers.delete(observer);
},
unsubscribe: (observer) => observers.delete(observer)
};
};
// 使用示例
const { observable, subscribe } = createObservable({ count: 0 });
// 订阅状态变化
const unsubscribe = subscribe((key, oldValue, newValue) => {
console.log(`${key} 从 ${oldValue} 变为 ${newValue}`);
});
observable.count = 1; // 输出: count 从 0 变为 1
observable.count = 2; // 输出: count 从 1 变为 2
// 取消订阅
unsubscribe();
observable.count = 3; // 无输出
4. API 请求拦截与缓存
javascript
const createCachedAPI = (apiClient) => {
const cache = new Map();
return new Proxy(apiClient, {
apply(target, thisArg, argumentsList) {
const [endpoint, params] = argumentsList;
const cacheKey = JSON.stringify({ endpoint, params });
// 检查缓存
if (cache.has(cacheKey)) {
console.log(`从缓存读取: ${endpoint}`);
return Promise.resolve(cache.get(cacheKey));
}
// 执行实际请求
return Reflect.apply(target, thisArg, argumentsList)
.then(response => {
console.log(`缓存响应: ${endpoint}`);
cache.set(cacheKey, response);
return response;
});
}
});
};
5. 性能监控与分析
javascript
const createProfiledObject = (obj) => {
const accessStats = new Map();
return new Proxy(obj, {
get(target, property) {
// 记录访问统计
const count = accessStats.get(property) || 0;
accessStats.set(property, count + 1);
// 记录访问时间
console.time(`访问 ${property}`);
const result = Reflect.get(target, property);
console.timeEnd(`访问 ${property}`);
return result;
},
// 获取性能报告
getStats() {
return Array.from(accessStats.entries())
.sort(([, a], [, b]) => b - a)
.map(([prop, count]) => ({ property: prop, accessCount: count }));
}
});
};
五、注意事项与最佳实践
1. 性能考虑
-
Proxy 会引入额外的性能开销,在性能敏感场景需谨慎使用
-
避免在频繁调用的热路径中使用复杂的代理逻辑
2. 透明性原则
-
确保代理对象的行为与原始对象保持一致
-
避免违反使用者的预期行为
3. 错误处理
javascript
const createSafeProxy = (target) => {
return new Proxy(target, {
get(target, property) {
try {
return Reflect.get(target, property);
} catch (error) {
console.error(`访问属性 ${property} 失败:`, error);
return null; // 或返回默认值
}
}
});
};
4. 撤销代理
javascript
const { proxy, revoke } = Proxy.revocable({}, {
get(target, property) {
return `访问 ${property}`;
}
});
console.log(proxy.message); // "访问 message"
revoke(); // 撤销代理
console.log(proxy.message); // TypeError: Cannot perform 'get' on a proxy that has been revoked
六、总结
ES6 Proxy 是一个强大的元编程工具,它允许开发者:
-
拦截和自定义对象操作,实现高级抽象
-
增强代码的安全性,通过验证和保护机制
-
实现复杂的编程模式,如观察者、缓存、AOP等
-
提高代码的可维护性,通过关注点分离
在实际应用中,Proxy 特别适合以下场景:
-
数据验证和格式化
-
访问控制和权限管理
-
缓存和性能优化
-
监控和日志记录
-
API 封装和适配
合理使用 Proxy 可以显著提升代码的灵活性和可维护性,但也要注意其性能影响和复杂性成本。