proxy 有哪些拦截器
JavaScript Proxy 拦截器大全
Proxy 提供了13种拦截器(traps),可以拦截目标对象的几乎所有基本操作。以下是完整的拦截器列表及其用途:
1. 基本操作拦截器
get(target, property, receiver)
- 拦截属性读取:
proxy.foo
或proxy['foo']
- 参数:目标对象、属性名、Proxy 或继承对象
set(target, property, value, receiver)
- 拦截属性设置:
proxy.foo = bar
或proxy['foo'] = bar
- 参数:目标对象、属性名、属性值、Proxy 或继承对象
- 需要返回布尔值表示是否设置成功
has(target, property)
- 拦截
in
操作符:'foo' in proxy
- 参数:目标对象、属性名
- 需要返回布尔值
2. 函数调用拦截器
apply(target, thisArg, argumentsList)
- 拦截函数调用:
proxy(...args)
- 参数:目标函数、this 值、参数列表
construct(target, argumentsList, newTarget)
- 拦截
new
操作:new proxy(...args)
- 参数:目标类、参数列表、最初被调用的构造函数
3. 对象属性管理拦截器
defineProperty(target, property, descriptor)
- 拦截
Object.defineProperty()
- 参数:目标对象、属性名、属性描述符
- 需要返回布尔值表示是否成功
deleteProperty(target, property)
- 拦截
delete
操作:delete proxy.foo
- 参数:目标对象、属性名
- 需要返回布尔值表示是否成功
getOwnPropertyDescriptor(target, property)
- 拦截
Object.getOwnPropertyDescriptor()
- 参数:目标对象、属性名
4. 原型相关拦截器
getPrototypeOf(target)
- 拦截
Object.getPrototypeOf()
- 参数:目标对象
setPrototypeOf(target, prototype)
- 拦截
Object.setPrototypeOf()
- 参数:目标对象、新原型对象
- 需要返回布尔值表示是否成功
5. 可扩展性相关拦截器
isExtensible(target)
- 拦截
Object.isExtensible()
- 参数:目标对象
- 需要返回布尔值
preventExtensions(target)
- 拦截
Object.preventExtensions()
- 参数:目标对象
- 需要返回布尔值表示是否成功
6. 属性枚举拦截器
ownKeys(target)
- 拦截:
Object.getOwnPropertyNames()
Object.getOwnPropertySymbols()
Object.keys()
for...in
循环
- 参数:目标对象
- 需要返回数组
7. 完整拦截器示例
const handler = {
// 基本操作
get: function(target, prop, receiver) {
console.log(`GET ${prop}`);
return Reflect.get(...arguments);
},
set: function(target, prop, value, receiver) {
console.log(`SET ${prop}=${value}`);
return Reflect.set(...arguments);
},
has: function(target, prop) {
console.log(`HAS ${prop}?`);
return Reflect.has(...arguments);
},
// 函数调用
apply: function(target, thisArg, argumentsList) {
console.log(`CALL with`, argumentsList);
return Reflect.apply(...arguments);
},
construct: function(target, argumentsList, newTarget) {
console.log(`NEW with`, argumentsList);
return Reflect.construct(...arguments);
},
// 属性管理
defineProperty: function(target, prop, descriptor) {
console.log(`DEFINE ${prop}`, descriptor);
return Reflect.defineProperty(...arguments);
},
deleteProperty: function(target, prop) {
console.log(`DELETE ${prop}`);
return Reflect.deleteProperty(...arguments);
},
getOwnPropertyDescriptor: function(target, prop) {
console.log(`DESCRIBE ${prop}`);
return Reflect.getOwnPropertyDescriptor(...arguments);
},
// 原型相关
getPrototypeOf: function(target) {
console.log('GET PROTOTYPE');
return Reflect.getPrototypeOf(...arguments);
},
setPrototypeOf: function(target, proto) {
console.log('SET PROTOTYPE', proto);
return Reflect.setPrototypeOf(...arguments);
},
// 可扩展性
isExtensible: function(target) {
console.log('IS EXTENSIBLE?');
return Reflect.isExtensible(...arguments);
},
preventExtensions: function(target) {
console.log('PREVENT EXTENSIONS');
return Reflect.preventExtensions(...arguments);
},
// 属性枚举
ownKeys: function(target) {
console.log('GET OWN KEYS');
return Reflect.ownKeys(...arguments);
}
};
const proxy = new Proxy({}, handler);
拦截器使用场景总结
拦截器 | 典型应用场景 |
---|---|
get |
访问控制、惰性加载、数据转换 |
set |
数据验证、自动持久化、变更通知 |
has |
隐藏属性、权限控制 |
apply |
函数调用包装、日志记录 |
construct |
单例模式、实例验证 |
defineProperty |
属性定义限制 |
deleteProperty |
防止重要属性被删除 |
getPrototypeOf |
隐藏真实原型 |
setPrototypeOf |
阻止原型修改 |
ownKeys |
过滤属性枚举 |
注意事项
- 不是所有拦截器都必须实现,只需实现需要的部分
- 在拦截器中通常应该使用
Reflect
对应的方法来完成默认行为 - 某些拦截器需要返回布尔值表示操作是否成功
- 过度使用 Proxy 可能会影响性能
- 某些操作无法被拦截,如
typeof
、instanceof
等
Proxy 的这些拦截器提供了强大的元编程能力,可以用来实现各种高级模式如数据绑定、不可变对象、观察者模式等。
Reflect
JavaScript Reflect 对象详解
Reflect
是 ES6 引入的一个内置对象,它提供了一系列静态方法,这些方法与 Proxy 的拦截器(traps)一一对应,用于执行对象的基本操作。
Reflect 的主要作用
- 提供操作对象的统一 API:将一些原本分散的操作(如
Object.defineProperty
、delete
等)统一到Reflect
对象上 - 与 Proxy 配合使用:在 Proxy 拦截器中,通常使用
Reflect
方法来执行默认行为 - 更合理的返回值:相比一些旧方法,
Reflect
方法有更合理的返回值(如返回布尔值表示操作是否成功)
Reflect 的常用方法
1. Reflect.get(target, propertyKey[, receiver])
获取对象属性的值
const obj = { x: 1, y: 2 };
console.log(Reflect.get(obj, 'x')); // 1
2. Reflect.set(target, propertyKey, value[, receiver])
设置对象属性的值
const obj = {};
Reflect.set(obj, 'name', 'Alice');
console.log(obj.name); // "Alice"
3. Reflect.has(target, propertyKey)
检查对象是否包含某属性(相当于 in
操作符)
const obj = { name: 'Bob' };
console.log(Reflect.has(obj, 'name')); // true
console.log(Reflect.has(obj, 'age')); // false
4. Reflect.deleteProperty(target, propertyKey)
删除对象属性(相当于 delete
操作符)
const obj = { x: 1, y: 2 };
Reflect.deleteProperty(obj, 'x');
console.log(obj); // { y: 2 }
5. Reflect.construct(target, argumentsList[, newTarget])
相当于 new
操作符
class Person {
constructor(name) {
this.name = name;
}
}
const p = Reflect.construct(Person, ['Alice']);
console.log(p.name); // "Alice"
6. Reflect.apply(func, thisArg, args)
调用函数(类似于 Function.prototype.apply
)
function sum(a, b) {
return a + b;
}
console.log(Reflect.apply(sum, null, [1, 2])); // 3
7. Reflect.defineProperty(target, propertyKey, attributes)
定义对象属性(类似于 Object.defineProperty
)
const obj = {};
Reflect.defineProperty(obj, 'x', { value: 1, writable: false });
console.log(obj.x); // 1
8. Reflect.getOwnPropertyDescriptor(target, propertyKey)
获取属性描述符(类似于 Object.getOwnPropertyDescriptor
)
const obj = { x: 1 };
const desc = Reflect.getOwnPropertyDescriptor(obj, 'x');
console.log(desc.value); // 1
9. Reflect.getPrototypeOf(target)
获取对象原型(类似于 Object.getPrototypeOf
)
const obj = {};
console.log(Reflect.getPrototypeOf(obj) === Object.prototype); // true
10. Reflect.setPrototypeOf(target, prototype)
设置对象原型(类似于 Object.setPrototypeOf
)
const obj = {};
const proto = { method() {} };
Reflect.setPrototypeOf(obj, proto);
console.log(obj.method); // function method() {}
Reflect 与 Proxy 的关系
Reflect
方法与 Proxy
的拦截器一一对应,通常在 Proxy 拦截器中使用 Reflect
方法来执行默认行为:
const proxy = new Proxy({}, {
get(target, prop, receiver) {
console.log(`Getting ${prop}`);
return Reflect.get(target, prop, receiver);
},
set(target, prop, value, receiver) {
console.log(`Setting ${prop} to ${value}`);
return Reflect.set(target, prop, value, receiver);
}
});
为什么使用 Reflect
-
更合理的返回值:许多
Reflect
方法返回布尔值表示操作是否成功,比旧方法更合理- 例如:
Reflect.defineProperty
返回布尔值,而Object.defineProperty
返回对象或抛出错误
- 例如:
-
函数式编程:所有方法都是函数形式,更适合函数式编程风格
-
与 Proxy 完美配合:在 Proxy 拦截器中,使用
Reflect
可以方便地执行默认行为 -
统一 API:将原本分散在不同地方的 API 统一到
Reflect
对象上
示例:使用 Reflect 实现简单的数据绑定
const data = { name: 'Alice', age: 25 };
const listeners = new Set();
const proxy = new Proxy(data, {
set(target, prop, value, receiver) {
const success = Reflect.set(target, prop, value, receiver);
if (success) {
listeners.forEach(listener => listener());
}
return success;
}
});
function addListener(listener) {
listeners.add(listener);
}
addListener(() => console.log('Data changed!'));
proxy.name = 'Bob'; // 输出: "Data changed!"
Reflect
是现代 JavaScript 元编程的重要组成部分,与 Proxy
配合使用可以实现许多高级功能。
如何只拦截劫持自定义的对象属性,而不是对象自带的属性
在 Proxy get 拦截器中区分业务属性和内置属性
要在 Proxy 的 get
拦截器中只处理业务属性而忽略对象的内置属性(如原型方法、Symbol 属性等),可以采用以下几种策略:
方法一:使用属性前缀区分
const handler = {
get(target, property, receiver) {
// 只处理以特定前缀开头的属性(如'$')
if (typeof property === 'string' && property.startsWith('$')) {
console.log(`访问业务属性: ${property}`);
return Reflect.get(target, property, receiver);
}
// 其他属性直接返回
return Reflect.get(target, property, receiver);
}
};
const obj = { $name: '业务数据', age: 30, toString: '测试' };
const proxy = new Proxy(obj, handler);
console.log(proxy.$name); // 会触发拦截
console.log(proxy.age); // 不会触发拦截
console.log(proxy.toString); // 不会触发拦截
方法二:使用白名单机制
const businessProps = new Set(['name', 'age', 'email']);
const handler = {
get(target, property, receiver) {
// 只处理白名单中的属性
if (businessProps.has(property)) {
console.log(`访问业务属性: ${property}`);
return Reflect.get(target, property, receiver);
}
return Reflect.get(target, property, receiver);
}
};
const obj = { name: 'Alice', age: 25, toString: '测试' };
const proxy = new Proxy(obj, handler);
console.log(proxy.name); // 会触发拦截
console.log(proxy.age); // 会触发拦截
console.log(proxy.toString); // 不会触发拦截
方法三:排除内置属性和方法
const handler = {
get(target, property, receiver) {
// 排除Symbol属性、原型方法和内置属性
if (
typeof property !== 'symbol' &&
!Object.prototype.hasOwnProperty.call(Object.prototype, property) &&
!(property in Function.prototype)
) {
console.log(`访问业务属性: ${property}`);
return Reflect.get(target, property, receiver);
}
return Reflect.get(target, property, receiver);
}
};
const obj = {
name: '业务数据',
[Symbol('test')]: 'Symbol值',
toString: '覆盖的toString'
};
const proxy = new Proxy(obj, handler);
console.log(proxy.name); // 会触发拦截
console.log(proxy.toString); // 不会触发拦截(虽然被覆盖但仍被视为内置属性)
console.log(proxy[Object.getOwnPropertySymbols(obj)[0]]); // 不会触发拦截
方法四:使用属性描述符判断
const handler = {
get(target, property, receiver) {
const desc = Object.getOwnPropertyDescriptor(target, property);
// 只处理可枚举的自有属性
if (desc && desc.enumerable) {
console.log(`访问业务属性: ${property}`);
return Reflect.get(target, property, receiver);
}
return Reflect.get(target, property, receiver);
}
};
const obj = Object.create(null, {
name: { value: 'Alice', enumerable: true },
age: { value: 25, enumerable: false },
[Symbol('test')]: { value: 'Symbol', enumerable: true }
});
const proxy = new Proxy(obj, handler);
console.log(proxy.name); // 会触发拦截
console.log(proxy.age); // 不会触发拦截
console.log(proxy[Object.getOwnPropertySymbols(obj)[0]]); // 会触发拦截
最佳实践建议
- 明确业务属性定义:最好预先定义哪些属性是业务属性(如使用前缀、白名单等)
- 考虑性能:复杂的判断逻辑会影响性能,在频繁访问的场景要谨慎
- 处理继承属性:如果需要处理继承来的业务属性,需要额外判断
- Symbol属性处理:明确是否需要拦截Symbol属性
完整示例:结合多种策略
const BUSINESS_PREFIX = '$';
const builtInProps = new Set(
Object.getOwnPropertyNames(Object.prototype)
.concat(Object.getOwnPropertyNames(Function.prototype))
);
const handler = {
get(target, property, receiver) {
// 1. 排除Symbol属性
if (typeof property === 'symbol') {
return Reflect.get(target, property, receiver);
}
// 2. 排除内置属性
if (builtInProps.has(property)) {
return Reflect.get(target, property, receiver);
}
// 3. 只处理特定前缀的业务属性
if (property.startsWith(BUSINESS_PREFIX)) {
console.log(`处理业务属性 ${property}`);
const value = Reflect.get(target, property, receiver);
// 这里可以添加业务逻辑处理
return value.toUpperCase(); // 示例:转换为大写
}
// 其他属性直接返回
return Reflect.get(target, property, receiver);
}
};
const obj = {
$name: 'alice',
$id: '123',
age: 30,
toString: 'test',
[Symbol('test')]: 'symbol value'
};
const proxy = new Proxy(obj, handler);
console.log(proxy.$name); // 处理业务属性 $name → ALICE
console.log(proxy.age); // 30 (不处理)
console.log(proxy.toString); // "test" (不处理)
console.log(proxy[Object.getOwnPropertySymbols(obj)[0]]); // "symbol value" (不处理)
这种方法可以精确控制哪些属性需要被拦截处理,而哪些属性应该保持原样。