怎样排查“到底是谁改了这个对象”?

先理解原理和示例,再练习完整口述与追问。

开发环境用 Proxy 拦截 set/deleteProperty 打印调用栈;单个属性可用访问器设置断点;Object.freeze 让严格模式下的修改直接抛错;DOM 用“属性修改时中断”;框架用 Vue 的 onTrigger 或 Redux DevTools 看是谁触发了变化。

修改对象本身不会留下记录,需要在写入发生的那一刻打断或记录调用栈。Proxy 可以拦截通过代理进行的写入和删除,在陷阱里 console.trace() 或写 debugger,调用栈就会指向修改者。陷阱内用 Reflect.set(target, key, value, receiver) 完成默认行为,它会正确处理继承来的 setter 和 this,并返回是否成功。

只关心某个属性时,可用 Object.defineProperty 把它改成访问器,在 setter 里打断点。Object.freeze 后,严格模式代码修改会在出错位置抛 TypeError,适合测试环境快速定位。Vue3 的 watch/computed 支持 onTrack、onTrigger 调试选项,Pinia 和 Redux 的 DevTools 能显示每次修改来自哪个 action。

js
function watchMutations(target, label = 'object') {
  return new Proxy(target, {
    set(obj, key, value, receiver) {
      console.trace(`[${label}] set ${String(key)}`, value)
      return Reflect.set(obj, key, value, receiver)
    },
    deleteProperty(obj, key) {
      console.trace(`[${label}] delete ${String(key)}`)
      return Reflect.deleteProperty(obj, key)
    }
  })
}
const config = watchMutations({ theme: 'light' }, 'config')
config.theme = 'dark' // 控制台打印修改位置的调用栈

Proxy 只能拦截经过代理引用的修改,别处仍持有原始对象引用时会绕过;嵌套对象需要递归包装。调试代码不要带上线,Proxy 和调用栈打印都有性能开销。

我会在写入发生时拿到调用栈。开发环境可以把对象包成 Proxy,在 set 和 deleteProperty 陷阱里打印 trace,再用 Reflect 完成默认写入;只盯一个属性就用访问器加断点;测试中也可以 freeze 对象,让修改处直接抛错。如果是框架状态,就用 Vue 的 onTrigger 或 Pinia、Redux DevTools 看修改来源。要注意 Proxy 只拦截通过代理的写入,原始引用会绕过。