← 中文资源

简体中文版本

支持 Map、Set 与循环引用的深度相等

按结构比较嵌套值,支持日期、正则、无序 Map 与 Set,并检测循环引用而不依赖 JSON 序列化。

阅读英文原文

功能说明

该示例提供比 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;
}