JSON 深度比较与 Diff
问题
实现一个 deepEqual 函数,深度比较两个值是否相等。进阶:实现一个 diff 函数,找出两个对象之间的差异。
示例:
deepEqual({ a: 1, b: { c: 2 } }, { a: 1, b: { c: 2 } }); // true
deepEqual({ a: 1 }, { a: '1' }); // false
diff(
{ a: 1, b: 2, c: { d: 3 } },
{ a: 1, b: 4, c: { d: 5 }, e: 6 }
);
// [
// { type: 'changed', path: 'b', oldValue: 2, newValue: 4 },
// { type: 'changed', path: 'c.d', oldValue: 3, newValue: 5 },
// { type: 'added', path: 'e', newValue: 6 }
// ]
前端应用
- React 中的
shouldComponentUpdate/React.memo需要比较 props - 状态管理库的脏检查
- 表单变更检测(对比初始值和当前值)
- 配置 diff 展示
答案 - deepEqual
方法一:完整实现(推荐)
deepEqual.ts
function deepEqual(a: unknown, b: unknown): boolean {
// 1. 严格相等(处理原始类型和同一引用)
if (a === b) return true;
// 2. NaN 特殊处理
if (typeof a === 'number' && typeof b === 'number') {
return Number.isNaN(a) && Number.isNaN(b);
}
// 3. null / undefined / 类型不同
if (a === null || b === null) return false;
if (typeof a !== typeof b) return false;
if (typeof a !== 'object') return false;
// 4. 数组比较
const aArr = Array.isArray(a);
const bArr = Array.isArray(b);
if (aArr !== bArr) return false;
if (aArr && bArr) {
if (a.length !== b.length) return false;
return a.every((item, i) => deepEqual(item, (b as any[])[i]));
}
// 5. 对象比较
const aObj = a as Record<string, unknown>;
const bObj = b as Record<string, unknown>;
const aKeys = Object.keys(aObj);
const bKeys = Object.keys(bObj);
if (aKeys.length !== bKeys.length) return false;
return aKeys.every(key => {
return Object.prototype.hasOwnProperty.call(bObj, key) &&
deepEqual(aObj[key], bObj[key]);
});
}
测试用例:
deepEqual(1, 1); // true
deepEqual(NaN, NaN); // true
deepEqual([1, [2, 3]], [1, [2, 3]]); // true
deepEqual({ a: 1 }, { a: 1 }); // true
deepEqual({ a: 1, b: 2 }, { b: 2, a: 1 }); // true(key 顺序无关)
deepEqual(null, undefined); // false
deepEqual({ a: undefined }, {}); // false(key 不同)
方法二:支持 Date、RegExp、Map、Set
deepEqualAdvanced.ts
function deepEqual(a: unknown, b: unknown): boolean {
if (a === b) return true;
if (a === null || b === null) return false;
if (typeof a !== typeof b) return false;
// 特殊对象类型
if (a instanceof Date && b instanceof Date) {
return a.getTime() === b.getTime();
}
if (a instanceof RegExp && b instanceof RegExp) {
return a.toString() === b.toString();
}
if (a instanceof Map && b instanceof Map) {
if (a.size !== b.size) return false;
for (const [key, val] of a) {
if (!b.has(key) || !deepEqual(val, b.get(key))) return false;
}
return true;
}
if (a instanceof Set && b instanceof Set) {
if (a.size !== b.size) return false;
for (const val of a) {
if (!b.has(val)) return false;
}
return true;
}
if (typeof a !== 'object') {
return Number.isNaN(a as number) && Number.isNaN(b as number);
}
// 通用对象比较
const aObj = a as Record<string, unknown>;
const bObj = b as Record<string, unknown>;
const keys = Object.keys(aObj);
if (keys.length !== Object.keys(bObj).length) return false;
return keys.every(key =>
Object.prototype.hasOwnProperty.call(bObj, key) &&
deepEqual(aObj[key], bObj[key])
);
}
答案 - Diff
实现对象 Diff
diff.ts
interface DiffResult {
type: 'added' | 'removed' | 'changed';
path: string;
oldValue?: unknown;
newValue?: unknown;
}
function diff(
oldObj: Record<string, any>,
newObj: Record<string, any>,
prefix: string = ''
): DiffResult[] {
const results: DiffResult[] = [];
const allKeys = new Set([
...Object.keys(oldObj),
...Object.keys(newObj)
]);
for (const key of allKeys) {
const path = prefix ? `${prefix}.${key}` : key;
const oldVal = oldObj[key];
const newVal = newObj[key];
// 新增
if (!(key in oldObj)) {
results.push({ type: 'added', path, newValue: newVal });
continue;
}
// 删除
if (!(key in newObj)) {
results.push({ type: 'removed', path, oldValue: oldVal });
continue;
}
// 类型不同
if (typeof oldVal !== typeof newVal || Array.isArray(oldVal) !== Array.isArray(newVal)) {
results.push({ type: 'changed', path, oldValue: oldVal, newValue: newVal });
continue;
}
// 都是对象,递归比较
if (typeof oldVal === 'object' && oldVal !== null && !Array.isArray(oldVal)) {
results.push(...diff(oldVal, newVal, path));
continue;
}
// 原始值比较
if (oldVal !== newVal) {
results.push({ type: 'changed', path, oldValue: oldVal, newValue: newVal });
}
}
return results;
}
测试:
const changes = diff(
{ a: 1, b: 2, c: { d: 3 }, f: 'hello' },
{ a: 1, b: 4, c: { d: 5 }, e: 6 }
);
// [
// { type: 'changed', path: 'b', oldValue: 2, newValue: 4 },
// { type: 'changed', path: 'c.d', oldValue: 3, newValue: 5 },
// { type: 'removed', path: 'f', oldValue: 'hello' },
// { type: 'added', path: 'e', newValue: 6 }
// ]
常见面试追问
Q1: 如何处理循环引用?
答案:用 WeakSet 检测已访问对象:
function deepEqual(
a: unknown, b: unknown,
seen = new WeakSet()
): boolean {
if (a === b) return true;
if (typeof a !== 'object' || a === null ||
typeof b !== 'object' || b === null) return false;
if (seen.has(a as object)) return true; // 已经比较过,视为相等
seen.add(a as object);
// ... 其余比较逻辑
}
Q2: React.memo 的浅比较和深比较区别?
答案:
| 浅比较 (shallowEqual) | 深比较 (deepEqual) | |
|---|---|---|
| 层级 | 只比较第一层 | 递归所有层 |
| 性能 | , n 为 key 数量 | , n 为所有叶子节点 |
| 默认 | React.memo 默认 | 需自定义 |
// React.memo 默认浅比较
React.memo(Component);
// 自定义深比较(谨慎使用,性能开销大)
React.memo(Component, (prevProps, nextProps) => deepEqual(prevProps, nextProps));
性能警告
深比较的代价可能比重新渲染还大。只在 props 嵌套深但更新不频繁时考虑使用。
Q3: JSON.stringify 可以用来比较吗?
答案:可以但有局限:
// 简单场景可用
JSON.stringify(a) === JSON.stringify(b);
// 局限:
// ❌ key 顺序不同会判为不等: {a:1,b:2} vs {b:2,a:1}
// ❌ undefined 会被忽略: {a: undefined} vs {}
// ❌ NaN → null: {a: NaN} → {a: null}
// ❌ 函数、Symbol 被忽略
// ❌ Date 变成字符串
// ❌ 循环引用抛错