**结论先给:生产里优先用 fast-deep-equal 或 lodash.isEqual,只有纯 JSON 结构的数据才值得手写递归;JSON.stringify 只适合临时脚本和一行调试,不要出现在热路径里。**

你贴到一半的实现补完是这样(原型和长度都过了之后,只需要按 a 的键逐个校验):

for (const k of Object.keys(a)) {
  if (!Object.prototype.hasOwnProperty.call(b, k)) return false
  if (!deepEqual(a[k], b[k], seen)) return false
}
return true

几个细节值得抠一下:

  1. seen.get(a) === b 之后直接 seen.set(a, b) 会在 a 已映射到别的对象时覆盖旧记录。更稳的是先 if (seen.has(a)) return seen.get(a) === b,宁可误判「不相等」也别放过不相等——业务上取错比漏判安全。
  2. seen 别在参数默认值里 new WeakMap(),每次调用都分配一个。改成 seen 默认 undefined,第一次进对象分支时才建,浅比较密集的场景能省一大截。
  3. Date 用 Object.is(a.getTime(), b.getTime()),否则两个 Invalid Date 会因为 NaN !== NaN 判成不相等。
  4. Map/Set 必须单独分支:Map 按 size + 逐对 key/value 下钻(key 也可能要深比);Set 没有顺序,只能 O(n²) 穷举,或者先按 id/引用排序再比。

性能上量级差别主要在前置短路,不在算法本身:Object.is(a, b) 命中同一引用、typeof 不同、原型不同、Object.keys().length 不同,这四道闸能拦掉绝大部分真实调用。JSON.stringify 慢的根本原因是要把整棵树序列化再分配一个大字符串,大对象上比带短路的递归慢一个数量级很常见。另外递归深度上千会爆栈,超深结构(比如树形评论)改用显式栈迭代。

最后提醒一个坑:Object.getPrototypeOf 严格相等会挡掉跨 realm 的对象(iframe、Node vm 里造的)和 Object.create(null) 对 {} 的情况,做通用工具函数时这条件要么放宽成「构造函数名 + 自有原型属性比对」,要么在文档里写清楚只接受同 realm 对象。