如何手写深度相等 deepEqual?需要注意哪些边界?
简化版
deepEqual 用来判断两个值结构和值是否相等。基础思路是:先用 Object.is 处理基本类型、NaN 和 -0;如果都是对象,再比较类型、数组长度、对象 key 数量,并递归比较每个属性。边界包括 Date、RegExp、Map、Set、循环引用、函数比较和原型差异。面试版通常先实现普通对象和数组,再说明扩展边界。
详细版
基础版可以覆盖大多数场景:
function deepEqual(a: any, b: any): boolean {
if (Object.is(a, b)) return true
if (typeof a !== 'object' || a === null || typeof b !== 'object' || b === null) return false
if (Array.isArray(a) !== Array.isArray(b)) return false
const keysA = Object.keys(a)
const keysB = Object.keys(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])) return false
}
return true
}
| 边界 | 处理方式 |
|---|---|
| NaN | Object.is |
| -0 | Object.is |
| Date | 比较时间戳 |
| 循环引用 | WeakMap 记录访问对 |
deepEqual 最重要的是先定义“相等”的语义,不同库对函数、原型、Map/Set 的处理可能不同。
完整版教学
一、深比较考什么
这题考递归、类型判断、对象属性遍历和边界意识。不要一上来就 JSON.stringify,那会漏掉很多情况。
例如对象 key 顺序、undefined、函数、Symbol、循环引用都会让 stringify 方案不可靠。
二、先处理基本类型
Object.is 比 === 更适合做第一步,因为它能正确区分 NaN 和 -0。
Object.is(NaN, NaN) // true
Object.is(0, -0) // false
如果第一步相等,就可以直接返回 true。
三、再处理 null 和对象类型
如果其中一个不是对象,说明基本类型不相等,直接 false。
if (typeof a !== 'object' || a === null) return false
if (typeof b !== 'object' || b === null) return false
注意 typeof null 是 "object",所以必须单独判断。
四、数组和普通对象
数组要比较长度和每一项;普通对象要比较 key 数量、key 是否都存在、每个值是否深度相等。
Object.keys 只拿可枚举自有字符串 key。如果要支持 Symbol,需要额外处理。
五、特殊对象怎么处理
Date、RegExp、Map、Set 都不是普通对象。基础版本可以不实现,但要说明。
| 类型 | 比较方式 |
|---|---|
| Date | getTime() |
| RegExp | source 和 flags |
| Map | size 和键值对 |
| Set | size 和成员 |
Map/Set 的深比较会更复杂,因为对象 key 和成员可能也要深比较。
六、循环引用怎么处理
如果对象互相引用,递归会死循环。可以用 WeakMap 记录已经比较过的对象对。
function deepEqualSafe(a: any, b: any, seen = new WeakMap<object, object>()) {
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)
return true
}
这里只展示思路,完整实现还要接上属性递归。
七、常见误区与追问
- 误区:JSON.stringify 可以做深比较。 它会受 key 顺序影响,也处理不了循环引用、函数、undefined 和 Symbol。
- 误区:对象 key 数量一样就相等。 还要比较 key 是否一致和值是否深度相等。
- 误区:数组也是对象,可以完全不单独处理。 数组顺序和长度语义明确,最好单独判断。
- 追问:NaN 怎么比较? 用
Object.is(NaN, NaN)返回 true。 - 追问:循环引用怎么办? 用 WeakMap 记录已比较对象对,避免无限递归。
- 追问:函数怎么比较? 通常按引用比较,不比较函数源码字符串。
八、加强记忆
deepEqual 记成“四步走”:Object.is、类型/null、数组/对象结构、递归属性。高级边界是特殊对象和循环引用,面试里主动说出这些就很加分。