阅读视图

发现新文章,点击刷新页面。

理解 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 作为键,完全没问题。

❌