JavaScript Proxy 和 Reflect
一、Proxy 基本概念
1. 什么是 Proxy
Proxy 是 ES6 引入的元编程特性,用于创建一个对象的代理,从而可以拦截和自定义对象的基本操作。
const target = {};
const handler = {
get(target, property) {
return `拦截读取: ${property}`;
}
};
const proxy = new Proxy(target, handler);
console.log(proxy.message); // "拦截读取: message"
2. 核心术语
- target:被代理的目标对象
- handler:包含拦截器(trap)的对象
- trap:拦截目标对象操作的函数
二、Proxy 拦截操作
1. 常用拦截器
属性访问拦截
const handler = {
get(target, prop) {
console.log(`读取属性 ${prop}`);
return target[prop];
},
set(target, prop, value) {
console.log(`设置属性 ${prop} = ${value}`);
target[prop] = value;
return true; // 表示设置成功
}
};
方法调用拦截
const handler = {
apply(target, thisArg, argumentsList) {
console.log(`调用函数,参数: ${argumentsList}`);
return target.apply(thisArg, argumentsList);
}
};
function sum(a, b) { return a + b; }
const proxySum = new Proxy(sum, handler);
proxySum(1, 2); // 输出日志后返回3
2. 完整拦截器列表
拦截器 | 触发时机 | 示例 |
---|---|---|
get | 读取属性 | proxy.x |
set | 设置属性 | proxy.x = 1 |
has | in 操作符 | 'x' in proxy |
deleteProperty | delete 操作 | delete proxy.x |
apply | 函数调用 | proxy() |
construct | new 操作 | new proxy() |
ownKeys | Object.keys()等 | Object.keys(proxy) |
getPrototypeOf | Object.getPrototypeOf() | Object.getPrototypeOf(proxy) |
setPrototypeOf | Object.setPrototypeOf() | Object.setPrototypeOf(proxy, proto) |
isExtensible | Object.isExtensible() | Object.isExtensible(proxy) |
preventExtensions | Object.preventExtensions() | Object.preventExtensions(proxy) |
getOwnPropertyDescriptor | Object.getOwnPropertyDescriptor() | Object.getOwnPropertyDescriptor(proxy, 'x') |
defineProperty | Object.defineProperty() | Object.defineProperty(proxy, 'x', desc) |
三、Proxy 高级应用
1. 数据验证
const validator = {
set(target, prop, value) {
if (prop === 'age') {
if (!Number.isInteger(value) || value < 0) {
throw new TypeError('年龄必须是正整数');
}
}
target[prop] = value;
return true;
}
};
const person = new Proxy({}, validator);
person.age = 30; // 正常
person.age = 'young'; // 抛出错误
2. 自动填充对象
const autoFiller = {
get(target, prop) {
if (!(prop in target)) {
target[prop] = {};
}
return target[prop];
}
};
const tree = new Proxy({}, autoFiller);
tree.branch1.branch2.leaf = 'value';
console.log(tree); // { branch1: { branch2: { leaf: 'value' } } }
3. 负数组索引
const negativeArray = arr => new Proxy(arr, {
get(target, prop, receiver) {
const index = parseInt(prop);
if (index < 0) {
prop = target.length + index;
}
return Reflect.get(target, prop, receiver);
}
});
const arr = negativeArray(['a', 'b', 'c']);
console.log(arr[-1]); // "c"
四、Reflect 基本概念
1. 什么是 Reflect
Reflect 是一个内置对象,提供拦截 JavaScript 操作的方法,这些方法与 Proxy 的拦截器一一对应。
const obj = { x: 1 };
console.log(Reflect.get(obj, 'x')); // 1
2. Reflect 的设计目的
-
统一操作 API:将语言内部操作(如
[[Get]]
、[[Set]]
)暴露为函数 - 与 Proxy 配合:每个 Proxy 拦截器都有对应的 Reflect 方法
- 替代部分 Object 方法:更合理的返回值设计
五、Reflect 主要方法
1. 基本操作方法
方法 | 等价操作 | 区别 |
---|---|---|
Reflect.get(target, prop) |
target[prop] |
支持 receiver |
Reflect.set(target, prop, value) |
target[prop] = value |
返回布尔值 |
Reflect.has(target, prop) |
prop in target |
- |
Reflect.deleteProperty(target, prop) |
delete target[prop] |
返回布尔值 |
Reflect.ownKeys(target) |
Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target)) |
- |
2. 对象扩展方法
方法 | 等价操作 | 区别 |
---|---|---|
Reflect.defineProperty(target, prop, desc) |
Object.defineProperty(target, prop, desc) |
返回布尔值 |
Reflect.getOwnPropertyDescriptor(target, prop) |
Object.getOwnPropertyDescriptor(target, prop) |
- |
Reflect.isExtensible(target) |
Object.isExtensible(target) |
- |
Reflect.preventExtensions(target) |
Object.preventExtensions(target) |
返回布尔值 |
3. 原型相关方法
const obj = {};
const proto = { x: 1 };
// 设置原型
Reflect.setPrototypeOf(obj, proto);
console.log(obj.x); // 1
// 获取原型
console.log(Reflect.getPrototypeOf(obj) === proto); // true
4. 函数调用方法
function greet(name) {
return `Hello, ${name}`;
}
// 函数调用
console.log(Reflect.apply(greet, null, ['Alice'])); // "Hello, Alice"
// 构造函数调用
class Person {}
const p = Reflect.construct(Person, []);
console.log(p instanceof Person); // true
六、Proxy 和 Reflect 配合使用
1. 默认转发模式
const proxy = new Proxy(target, {
get(target, prop, receiver) {
console.log(`GET ${prop}`);
return Reflect.get(target, prop, receiver);
},
set(target, prop, value, receiver) {
console.log(`SET ${prop}=${value}`);
return Reflect.set(target, prop, value, receiver);
}
});
2. 实现观察者模式
function createObservable(target, observer) {
return new Proxy(target, {
set(target, prop, value, receiver) {
const oldValue = target[prop];
const result = Reflect.set(target, prop, value, receiver);
if (result && oldValue !== value) {
observer(prop, oldValue, value);
}
return result;
}
});
}
const data = createObservable({}, (prop, oldVal, newVal) => {
console.log(`属性 ${prop} 从 ${oldVal} 变为 ${newVal}`);
});
data.x = 1; // 输出: "属性 x 从 undefined 变为 1"
data.x = 2; // 输出: "属性 x 从 1 变为 2"
七、实际应用场景
1. API 客户端封装
function createAPI(baseUrl) {
return new Proxy({}, {
get(target, endpoint) {
return async function(params) {
const url = `${baseUrl}/${endpoint}`;
const response = await fetch(url, {
method: 'POST',
body: JSON.stringify(params)
});
return response.json();
};
}
});
}
const api = createAPI('https://api.example.com');
const userData = await api.users({ id: 123 });
2. 不可变数据
function createImmutable(obj) {
return new Proxy(obj, {
set() {
throw new Error('不可修改');
},
deleteProperty() {
throw new Error('不可删除');
},
defineProperty() {
throw new Error('不可定义新属性');
}
});
}
const immutable = createImmutable({ x: 1 });
immutable.x = 2; // 抛出错误
3. 自动缓存
function createCached(fn) {
const cache = new Map();
return new Proxy(fn, {
apply(target, thisArg, args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key);
}
const result = Reflect.apply(target, thisArg, args);
cache.set(key, result);
return result;
}
});
}
const heavyCompute = createCached(x => {
console.log('计算中...');
return x * x;
});
heavyCompute(2); // 输出"计算中..."后返回4
heavyCompute(2); // 直接返回4,不输出
八、注意事项与最佳实践
1. Proxy 限制
- 目标对象不变:Proxy 不会修改目标对象本身
-
严格相等:
proxy === target
为 false - 内部插槽:某些内置对象(如 Date、Map)的内部插槽无法被拦截
2. 性能考虑
- 代理有开销:简单操作用原生方式更快
- 避免多层代理:代理的代理会显著降低性能
- 热路径优化:关键性能路径避免使用代理
3. 最佳实践
- 透明代理:尽量保持代理行为与目标对象一致
- 合理使用 Reflect:在拦截器中使用对应 Reflect 方法
- 明确文档:记录代理的自定义行为
- 错误处理:在拦截器中妥善处理异常
九、浏览器兼容性
1. 支持情况
- Proxy:主流浏览器及 Node.js 6+
- Reflect:主流浏览器及 Node.js 6+
2. 不支持的解决方案
// 简单的Proxy polyfill检查
if (typeof Proxy === 'undefined') {
console.warn('Proxy not supported, falling back to direct access');
Proxy = function(target) { return target; };
}
十、总结
Proxy 和 Reflect 为 JavaScript 提供了强大的元编程能力:
-
Proxy:
- 创建对象的代理,拦截基本操作
- 实现数据绑定、验证、观察等高级功能
- 自定义对象基本行为
-
Reflect:
- 提供操作对象的统一API
- 与Proxy拦截器一一对应
- 改进Object方法的返回值设计
两者结合使用可以实现许多传统JavaScript难以实现的高级模式,如:
- 透明的数据观察
- 自动化的资源管理
- 动态接口适配
- 领域特定语言(DSL)
这些特性在现代框架和库中有广泛应用,如Vue 3的响应式系统就基于Proxy实现。合理使用Proxy和Reflect可以显著提升代码的灵活性和可维护性。