怎样做可靠的深比较?JSON.stringify 为什么不行?
先理解原理和示例,再练习完整口述与追问。
回答要点
先用 Object.is 处理同一引用、NaN 和正负零。再按类型比较:日期比时间,正则比 source 和 flags,数组要区分空位,普通对象比原型和全部自有键,包括 Symbol。循环引用用 WeakMap 记住“正在比较的一对”。JSON.stringify 会受键顺序、丢字段和循环引用影响,只能用于双方约定好的纯 JSON。
原理拆解
=== 对 NaN 返回 false,并把 +0 与 -0 看成相等。Object.is 把这两个例外反过来,适合作为深比较的基本出口。对象要先看内部类型,因为两个 Date 没有自有数据属性,只比键会把不同时间比成相等。数组的空位和显式 undefined 对 map、JSON 的意义不同,应该用 in 区分。Map 的键用自身的 SameValueZero 相等,值再深比较;这个实现不把“内容相同但是另一个对象”的键当成同一个键。
键顺序不同的普通对象,只要自有键集合和每个值相等,就应该相等,这正是 JSON.stringify 做不到的地方:它还丢掉 undefined、函数和 Symbol,遇到循环引用抛错。比较时记录 seen.set(a, b),再次遇到同一对就当作相等,才能处理自己引用自己的结构,同时不会把两个开始比较过的不同对象误判成永远相等。
示例与推演
function deepEqual(a, b, seen = new WeakMap()) {
if (Object.is(a, b)) return true
if (typeof a !== 'object' || a === null || typeof b !== 'object' || b === null) return false
if (seen.get(a) === b) return true
seen.set(a, b)
if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b)) return false
if (a instanceof Date) return Object.is(a.getTime(), b.getTime())
if (a instanceof RegExp) return a.source === b.source && a.flags === b.flags
if (a instanceof Map) {
if (a.size !== b.size) return false
for (const [key, value] of a) {
if (!b.has(key) || !deepEqual(value, b.get(key), seen)) return false
}
return true
}
if (a instanceof Set) {
if (a.size !== b.size) return false
for (const value of a) if (!b.has(value)) return false
return true
}
if (Array.isArray(a)) {
if (a.length !== b.length) return false
for (let i = 0; i < a.length; i++) {
if ((i in a) !== (i in b)) return false
if ((i in a) && !deepEqual(a[i], b[i], seen)) return false
}
return true
}
const keysA = Reflect.ownKeys(a)
const keysB = Reflect.ownKeys(b)
if (keysA.length !== keysB.length) return false
for (const key of keysA) {
if (!Object.prototype.hasOwnProperty.call(b, key)) return false
if (!deepEqual(a[key], b[key], seen)) return false
}
return true
}
deepEqual(NaN, NaN) 为 true,deepEqual(+0, -0) 为 false。deepEqual([1,,3], [1, undefined, 3]) 为 false。const box = {}; box.self = box 与另一个同样结构的对象比较为 true。deepEqual(new Date(1), new Date(2)) 为 false。
易错点
这个实现不比较属性描述符,getter 会被读出当前值;数组上额外挂的非下标属性没有纳入;Set 里的对象按引用比较。业务对象如果有类方法,应先取出可序列化的数据再比,或者只比较约定字段。
参考回答可直接练习口述
深比较要先用 Object.is,这样 NaN 相等、正负零不相等。日期比时间戳,正则比模式和标志,数组用 in 区分空位和 undefined,普通对象比较原型以及包括 Symbol 在内的自有键。循环引用用 WeakMap 记下当前这一对,避免无限递归。JSON.stringify 受键顺序影响,还会丢掉 undefined、函数和 Symbol,循环引用直接报错,所以只能比较双方都保证是纯 JSON 的数据。