功能说明
该示例提供比 JSON.stringify 更可靠的深度相等判断。它处理原始值、日期、正则、数组、普通对象、Map 与 Set,其中 Map 与 Set 不要求顺序一致。通过 WeakMap 记录已比较的对象对,遇到循环引用时直接视为相等,避免无限递归。适用于测试断言、缓存键比较、状态变更检测与配置差异判断。
关键代码
入口先使用 Object.is,正确区分 NaN 与零的正负。只有两个值都是对象且原型相同才继续,避免把类实例与普通对象误判相等。seen 为每个左对象维护已配对的右对象集合,在递归前登记。日期按时间戳比较,正则比较 source 与 flags;Map 与 Set 采用无序匹配,逐个元素寻找结构相等的对应项;数组逐下标比较,普通对象比较自有键数量与每个键的值。
使用注意
Map 与 Set 的匹配是平方复杂度,大集合会明显变慢,必要时可先构造规范化键。无序匹配在含循环引用的集合中依赖已登记的配对,极端结构可能出现误判,业务若要严格证明等价应使用规范化序列化。函数不比较属性描述符、不可枚举属性以外的原型方法,也不支持 WeakMap、Promise 等内部槽对象。不同 realm 的同名对象因原型不同会被判为不等。
验证方式
测试嵌套对象、数组与 NaN 的相等;两个键顺序不同但内容相同的普通对象应相等;Map 与 Set 顺序打乱仍相等;日期与正则内容一致时相等;构造自引用对象与互相引用对象,断言不会栈溢出且相等;比较类实例与普通对象、不同原型对象应返回 false;验证循环引用中字段不同会返回 false。
代码示例
function isObject(value: unknown): value is Record<PropertyKey, unknown> {
return value !== null && typeof value === "object";
}
export function deepEqual(left: unknown, right: unknown): boolean {
return compare(left, right, new WeakMap<object, WeakSet<object>>());
}
function compare(
left: unknown,
right: unknown,
seen: WeakMap<object, WeakSet<object>>,
): boolean {
if (Object.is(left, right)) return true;
if (!isObject(left) || !isObject(right)) return false;
if (Object.getPrototypeOf(left) !== Object.getPrototypeOf(right)) return false;
let partners = seen.get(left);
if (partners?.has(right)) return true;
if (!partners) {
partners = new WeakSet<object>();
seen.set(left, partners);
}
partners.add(right);
if (left instanceof Date && right instanceof Date) {
return Object.is(left.getTime(), right.getTime());
}
if (left instanceof RegExp && right instanceof RegExp) {
return left.source === right.source && left.flags === right.flags;
}
if (left instanceof Map && right instanceof Map) {
if (left.size !== right.size) return false;
for (const [key, value] of left) {
let found = false;
for (const [otherKey, otherValue] of right) {
if (compare(key, otherKey, seen) && compare(value, otherValue, seen)) {
found = true;
break;
}
}
if (!found) return false;
}
return true;
}
if (left instanceof Set && right instanceof Set) {
if (left.size !== right.size) return false;
for (const value of left) {
let found = false;
for (const other of right) {
if (compare(value, other, seen)) {
found = true;
break;
}
}
if (!found) return false;
}
return true;
}
if (Array.isArray(left) || Array.isArray(right)) {
if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) {
return false;
}
for (let index = 0; index < left.length; index += 1) {
if (!compare(left[index], right[index], seen)) return false;
}
return true;
}
const leftKeys = Reflect.ownKeys(left);
const rightKeys = Reflect.ownKeys(right);
if (leftKeys.length !== rightKeys.length) return false;
for (const key of leftKeys) {
if (!Object.prototype.hasOwnProperty.call(right, key)) return false;
if (!compare(left[key], right[key], seen)) return false;
}
return true;
}