理解 Proxy 原理及如何拦截 Map、Set 等集合方法调用实现自定义拦截和日志——含示例代码解析
先理解 Proxy 的核心思想
Proxy 就像一个“拦截器”,它可以“监听”一个对象的操作,比如:
- 访问对象的属性(读取) → 触发
get拦截器 - 给对象的属性赋值(写入) → 触发
set拦截器 - 调用对象的方法 → 其实是先访问方法(触发
get),再执行它
但集合类型(Map、Set 等)不直接用属性赋值来写入数据
- Map 写入数据是调用它的
set(key, value)方法 - Set 写入数据是调用它的
add(value)方法 - 读取数据是调用 Map 的
get(key)或 Set 的has(value)方法
所以,我们想拦截“写入”操作,就要拦截这些方法的调用。
Proxy 怎么拦截方法调用?
- 当你访问
proxyMap.set,会触发 Proxy 的get拦截器,告诉你访问了set方法。 - 这时我们返回一个“包装函数”,这个函数内部可以插入自定义逻辑(比如打印日志),然后再调用原始的
set方法。 - 这样就实现了“拦截写入操作”。
具体示例:拦截 Map 的读取和写入
const map = new Map();
const handler = {
get(target, prop, receiver) {
// 访问属性或方法时触发
const origMethod = target[prop];
if (typeof origMethod === 'function') {
// 如果访问的是方法,返回一个包装函数
return function (...args) {
if (prop === 'set') {
console.log(`写入操作:set(${args[0]}, ${args[1]})`);
} else if (prop === 'get') {
console.log(`读取操作:get(${args[0]})`);
}
// 调用原始方法
return origMethod.apply(target, args);
};
}
// 访问普通属性,直接返回
return Reflect.get(target, prop, receiver);
}
};
const proxyMap = new Proxy(map, handler);
proxyMap.set('name', 'CodeMoss'); // 控制台输出:写入操作:set(name, CodeMoss)
console.log(proxyMap.get('name')); // 控制台输出:读取操作:get(name)
// 输出:CodeMoss
可以把它理解成:
- 访问
proxyMap.set→ Proxy 拦截,返回一个“带日志”的函数 - 调用这个函数时,先打印日志,再调用真正的
map.set
Set 也是类似的,只是写入方法叫 add,读取方法叫 has
const set = new Set();
const handler = {
get(target, prop, receiver) {
const origMethod = target[prop];
if (typeof origMethod === 'function') {
return function (...args) {
if (prop === 'add') {
console.log(`写入操作:add(${args[0]})`);
} else if (prop === 'has') {
console.log(`读取操作:has(${args[0]})`);
}
return origMethod.apply(target, args);
};
}
return Reflect.get(target, prop, receiver);
}
};
const proxySet = new Proxy(set, handler);
proxySet.add(123); // 控制台输出:写入操作:add(123)
console.log(proxySet.has(123)); // 控制台输出:读取操作:has(123)
// 输出:true
WeakMap 和 WeakSet 也一样,只是它们的键或值必须是对象,且不能遍历
const weakMap = new WeakMap();
const handler = {
get(target, prop, receiver) {
const origMethod = target[prop];
if (typeof origMethod === 'function') {
return function (...args) {
if (prop === 'set') {
console.log('WeakMap 写入操作,键:', args[0], '值:', args[1]);
} else if (prop === 'get') {
console.log('WeakMap 读取操作,键:', args[0]);
}
return origMethod.apply(target, args);
};
}
return Reflect.get(target, prop, receiver);
}
};
const proxyWeakMap = new Proxy(weakMap, handler);
const objKey = {};
proxyWeakMap.set(objKey, 'secret'); // 控制台输出:WeakMap 写入操作,键: {} 值: secret
console.log(proxyWeakMap.get(objKey)); // 控制台输出:WeakMap 读取操作,键: {}
// 输出:secret
总结
- Proxy 的
get拦截器拦截的是“属性访问”,方法调用是先访问方法再执行。 - 集合的写入和读取都是通过调用方法实现的,所以我们拦截方法访问,返回包装函数。
- 包装函数里可以插入自定义逻辑(日志、权限等),然后调用原始方法完成操作。
把 const objKey = {}; 换成 map”,把 WeakMap 的键从一个普通对象 {} 换成一个 Map 对象。
先说明一点:
WeakMap 的键必须是对象,而 Map 本身是一个对象(它是一个构造函数实例),所以理论上是可以作为 WeakMap 的键的。
可以这样写:
const weakMap = new WeakMap();
const handler = {
get(target, prop, receiver) {
const origMethod = target[prop];
if (typeof origMethod === 'function') {
return function (...args) {
if (prop === 'set') {
console.log('WeakMap 写入操作,键:', args[0], '值:', args[1]);
} else if (prop === 'get') {
console.log('WeakMap 读取操作,键:', args[0]);
}
return origMethod.apply(target, args);
};
}
return Reflect.get(target, prop, receiver);
}
};
const proxyWeakMap = new Proxy(weakMap, handler);
// 这里用 Map 作为键
const mapKey = new Map();
proxyWeakMap.set(mapKey, 'secret'); // 控制台输出:WeakMap 写入操作,键: Map {} 值: secret
console.log(proxyWeakMap.get(mapKey)); // 控制台输出:WeakMap 读取操作,键: Map {}
// 输出:secret
解释:
-
mapKey是一个 Map 实例,属于对象类型,可以作为 WeakMap 的键。 - WeakMap 允许任何对象作为键,包括普通对象、数组、函数、甚至 Map、Set 等实例。
- 用 Map 作为键,完全没问题。