跳到主要内容

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)
层级只比较第一层递归所有层
性能O(n)O(n), n 为 key 数量O(n)O(n), 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 变成字符串
// ❌ 循环引用抛错

相关链接