JavaScript 对象相等性判断详解

在 JavaScript 中,判断对象相等有多种方式,各有不同用途和特点。

1. 相等性比较运算符

1.1 严格相等 ===和 宽松相等 ==

js 复制代码
const obj1 = { a: 1 };
const obj2 = { a: 1 };
const obj3 = obj1;

console.log(obj1 === obj2);  // false
console.log(obj1 === obj3);  // true
console.log(obj1 == obj2);   // false
console.log(obj1 == obj3);   // true

特点

  • 比较的是引用地址,不是内容
  • 只有指向同一内存地址的对象才相等
  • 适用于判断是否是同一个对象

2. 深度比较方法

2.1 JSON.stringify(简单对象)

js 复制代码
function simpleDeepEqual(obj1, obj2) {
  return JSON.stringify(obj1) === JSON.stringify(obj2);
}

const obj1 = { a: 1, b: 2 };
const obj2 = { a: 1, b: 2 };
const obj3 = { b: 2, a: 1 };  // 属性顺序不同
const obj4 = { a: 1, b: null };

console.log(simpleDeepEqual(obj1, obj2));  // true
console.log(simpleDeepEqual(obj1, obj3));  // false!顺序不同
console.log(simpleDeepEqual(obj1, { a: 1, b: 2 }));  // true

局限性

  • 属性顺序敏感
  • 不能处理 undefinedfunctionSymbol循环引用
  • undefined会被忽略,NaN会变成 null
  • 日期对象会转为字符串
js 复制代码
// JSON.stringify 的问题
console.log(JSON.stringify({ a: undefined }));  // "{}"
console.log(JSON.stringify({ a: function() {} }));  // "{}"
console.log(JSON.stringify({ a: Symbol() }));  // "{}"
console.log(JSON.stringify({ a: NaN }));  // '{"a":null}'
console.log(JSON.stringify(new Date()));  // 日期字符串

2.2 递归深度比较

js 复制代码
function deepEqual(obj1, obj2) {
  // 1. 基本类型比较
  if (obj1 === obj2) return true;
  
  // 2. 检查 null
  if (obj1 == null || obj2 == null) return false;
  
  // 3. 检查类型
  if (typeof obj1 !== typeof obj2) return false;
  if (obj1.constructor !== obj2.constructor) return false;
  
  // 4. 特殊类型处理
  if (obj1 instanceof Date && obj2 instanceof Date) {
    return obj1.getTime() === obj2.getTime();
  }
  
  if (obj1 instanceof RegExp && obj2 instanceof RegExp) {
    return obj1.toString() === obj2.toString();
  }
  
  // 5. 数组比较
  if (Array.isArray(obj1)) {
    if (obj1.length !== obj2.length) return false;
    for (let i = 0; i < obj1.length; i++) {
      if (!deepEqual(obj1[i], obj2[i])) return false;
    }
    return true;
  }
  
  // 6. 对象比较
  if (typeof obj1 === 'object') {
    const keys1 = Object.keys(obj1);
    const keys2 = Object.keys(obj2);
    
    if (keys1.length !== keys2.length) return false;
    
    for (let key of keys1) {
      if (!obj2.hasOwnProperty(key)) return false;
      if (!deepEqual(obj1[key], obj2[key])) return false;
    }
    
    return true;
  }
  
  // 7. 其他类型
  return false;
}

优化版本

js 复制代码
function deepEqual(obj1, obj2, visited = new WeakMap()) {
  // 处理循环引用
  if (visited.has(obj1) && visited.get(obj1) === obj2) {
    return true;
  }
  visited.set(obj1, obj2);
  
  // 基本类型
  if (obj1 === obj2) return true;
  if (obj1 == null || obj2 == null) return false;
  if (typeof obj1 !== 'object' || typeof obj2 !== 'object') {
    return obj1 === obj2;
  }
  
  // 不同类型
  if (obj1.constructor !== obj2.constructor) return false;
  
  // 特殊对象
  if (obj1 instanceof Date) return obj1.getTime() === obj2.getTime();
  if (obj1 instanceof RegExp) return obj1.toString() === obj2.toString();
  if (obj1 instanceof Map || obj2 instanceof Map) {
    if (obj1.size !== obj2.size) return false;
    for (let [key, val] of obj1) {
      if (!obj2.has(key)) return false;
      if (!deepEqual(val, obj2.get(key), visited)) return false;
    }
    return true;
  }
  if (obj1 instanceof Set || obj2 instanceof Set) {
    if (obj1.size !== obj2.size) return false;
    for (let val of obj1) {
      if (!obj2.has(val)) return false;
    }
    return true;
  }
  
  // 普通对象/数组
  const keys1 = Object.keys(obj1);
  const keys2 = Object.keys(obj2);
  
  if (keys1.length !== keys2.length) return false;
  
  for (let key of keys1) {
    if (!obj2.hasOwnProperty(key)) return false;
    if (!deepEqual(obj1[key], obj2[key], visited)) return false;
  }
  
  return true;
}

3. 浅比较

3.1 浅比较(只比较第一层)

js 复制代码
function shallowEqual(obj1, obj2) {
  if (obj1 === obj2) return true;
  
  if (typeof obj1 !== 'object' || obj1 === null ||
      typeof obj2 !== 'object' || obj2 === null) {
    return false;
  }
  
  const keys1 = Object.keys(obj1);
  const keys2 = Object.keys(obj2);
  
  if (keys1.length !== keys2.length) return false;
  
  for (let key of keys1) {
    if (!obj2.hasOwnProperty(key) || obj1[key] !== obj2[key]) {
      return false;
    }
  }
  
  return true;
}

// 使用
const obj1 = { a: 1, b: { c: 2 } };
const obj2 = { a: 1, b: { c: 2 } };

console.log(shallowEqual(obj1, obj2));  // false(b 属性引用不同)
console.log(shallowEqual(obj1, obj1));  // true

3.2 React 风格的浅比较

js 复制代码
function reactShallowEqual(objA, objB) {
  if (Object.is(objA, objB)) return true;
  
  if (typeof objA !== 'object' || objA === null ||
      typeof objB !== 'object' || objB === null) {
    return false;
  }
  
  const keysA = Object.keys(objA);
  const keysB = Object.keys(objB);
  
  if (keysA.length !== keysB.length) return false;
  
  for (let i = 0; i < keysA.length; i++) {
    if (
      !Object.prototype.hasOwnProperty.call(objB, keysA[i]) ||
      !Object.is(objA[keysA[i]], objB[keysA[i]])
    ) {
      return false;
    }
  }
  
  return true;
}

4. 使用第三方库

4.1 Lodash

js 复制代码
// 安装: npm install lodash
import _ from 'lodash';

const obj1 = { a: 1, b: { c: 2 } };
const obj2 = { a: 1, b: { c: 2 } };

console.log(_.isEqual(obj1, obj2));  // true
console.log(_.isEqualWith(obj1, obj2, customizer));  // 自定义比较

4.2 deep-diff

js 复制代码
// 安装: npm install deep-diff
import diff from 'deep-diff';

const differences = diff(obj1, obj2);
if (!differences) {
  console.log('对象相同');
} else {
  console.log('差异:', differences);
}

5. ES6+ 新特性比较

5.1 Object.is

js 复制代码
// Object.is 用于比较值
console.log(Object.is(0, -0));       // false
console.log(Object.is(NaN, NaN));    // true
console.log(Object.is({}, {}));      // false(引用比较)

// 与 === 的区别
console.log(+0 === -0);              // true
console.log(NaN === NaN);            // false
console.log(Object.is(+0, -0));      // false
console.log(Object.is(NaN, NaN));    // true

5.2 Map 和 Set

js 复制代码
// Set 自动去重
const set1 = new Set([1, 2, 3]);
const set2 = new Set([1, 2, 3]);
console.log(set1 === set2);  // false
console.log(_.isEqual(set1, set2));  // true

// Map
const map1 = new Map([['a', 1]]);
const map2 = new Map([['a', 1]]);
console.log(map1 === map2);  // false
console.log(_.isEqual(map1, map2));  // true

6. 特殊对象比较

6.1 函数比较

js 复制代码
function func1() { return 1; }
function func2() { return 1; }
function func3 = func1;

console.log(func1 === func2);  // false
console.log(func1 === func3);  // true
console.log(func1.toString() === func2.toString());  // true(不推荐)

// 函数比较实用方法
function functionsEqual(fn1, fn2) {
  return fn1.toString() === fn2.toString();
}

6.2 类实例比较

js 复制代码
class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  
  equals(other) {
    if (!(other instanceof Person)) return false;
    return this.name === other.name && this.age === other.age;
  }
}

const p1 = new Person('Alice', 30);
const p2 = new Person('Alice', 30);
const p3 = new Person('Bob', 25);

console.log(p1 === p2);      // false
console.log(p1.equals(p2));  // true
console.log(p1.equals(p3));  // false

7. 性能比较

js 复制代码
function performanceTest() {
  const obj1 = { a: 1, b: 2, c: { d: 3, e: { f: 4 } } };
  const obj2 = { a: 1, b: 2, c: { d: 3, e: { f: 4 } } };
  const obj3 = { a: 1, b: 2, c: { d: 3, e: { f: 5 } } };
  
  const iterations = 10000;
  
  // 1. JSON.stringify
  console.time('JSON.stringify');
  for (let i = 0; i < iterations; i++) {
    JSON.stringify(obj1) === JSON.stringify(obj2);
  }
  console.timeEnd('JSON.stringify');
  
  // 2. 递归深度比较
  console.time('递归深度比较');
  for (let i = 0; i < iterations; i++) {
    deepEqual(obj1, obj2);
  }
  console.timeEnd('递归深度比较');
  
  // 3. Lodash
  console.time('Lodash');
  for (let i = 0; i < iterations; i++) {
    _.isEqual(obj1, obj2);
  }
  console.timeEnd('Lodash');
  
  // 4. 浅比较
  console.time('浅比较');
  for (let i = 0; i < iterations; i++) {
    shallowEqual(obj1, obj2);
  }
  console.timeEnd('浅比较');
}

// 结果通常:
// 浅比较最快
// JSON.stringify 次之
// Lodash 再次
// 递归深度比较最慢

8. 实用工具函数

8.1 通用深度比较

js 复制代码
function isDeepEqual(obj1, obj2, options = {}) {
  const {
    strict = true,           // 严格比较
    checkSymbols = false,     // 检查 Symbol
    skipKeys = [],            // 跳过的键
  } = options;
  
  // 快速路径
  if (obj1 === obj2) return true;
  if (obj1 == null || obj2 == null) return false;
  
  const type1 = typeof obj1;
  const type2 = typeof obj2;
  if (type1 !== type2) return false;
  
  // 基本类型
  if (type1 !== 'object' && type1 !== 'function') {
    if (strict) {
      return Object.is(obj1, obj2);
    }
    return obj1 == obj2;
  }
  
  // 构造函数比较
  if (obj1.constructor !== obj2.constructor) return false;
  
  // 特殊对象处理
  if (obj1 instanceof Date) {
    return obj1.getTime() === obj2.getTime();
  }
  
  if (obj1 instanceof RegExp) {
    return obj1.toString() === obj2.toString();
  }
  
  if (obj1 instanceof Map) {
    if (obj1.size !== obj2.size) return false;
    for (let [key, val] of obj1) {
      if (!obj2.has(key) || !isDeepEqual(val, obj2.get(key), options)) {
        return false;
      }
    }
    return true;
  }
  
  if (obj1 instanceof Set) {
    if (obj1.size !== obj2.size) return false;
    for (let val of obj1) {
      if (!obj2.has(val)) return false;
    }
    return true;
  }
  
  // 收集所有键
  const keys1 = [...Object.keys(obj1)];
  const keys2 = [...Object.keys(obj2)];
  
  if (checkSymbols) {
    keys1.push(...Object.getOwnPropertySymbols(obj1));
    keys2.push(...Object.getOwnPropertySymbols(obj2));
  }
  
  // 过滤跳过的键
  const filteredKeys1 = keys1.filter(key => !skipKeys.includes(key));
  const filteredKeys2 = keys2.filter(key => !skipKeys.includes(key));
  
  if (filteredKeys1.length !== filteredKeys2.length) return false;
  
  for (let key of filteredKeys1) {
    if (!obj2.hasOwnProperty(key)) return false;
    if (!isDeepEqual(obj1[key], obj2[key], options)) return false;
  }
  
  return true;
}

8.2 比较指定路径

js 复制代码
function compareByPath(obj1, obj2, paths) {
  for (let path of paths) {
    const val1 = get(obj1, path);
    const val2 = get(obj2, path);
    if (!isDeepEqual(val1, val2)) {
      return false;
    }
  }
  return true;
}

function get(obj, path) {
  return path.split('.').reduce((acc, key) => acc && acc[key], obj);
}

// 使用
const obj1 = { a: 1, b: { c: 2, d: 3 }, e: 4 };
const obj2 = { a: 1, b: { c: 2, d: 5 }, e: 4 };

console.log(compareByPath(obj1, obj2, ['a', 'b.c']));  // true
console.log(compareByPath(obj1, obj2, ['a', 'b.d']));  // false

9. 常见场景

9.1 React/PureComponent

js 复制代码
// React 的 shouldComponentUpdate
class MyComponent extends React.PureComponent {
  // 自动进行浅比较
}

// 自定义 shouldComponentUpdate
class MyComponent extends React.Component {
  shouldComponentUpdate(nextProps, nextState) {
    return !shallowEqual(this.props, nextProps) ||
           !shallowEqual(this.state, nextState);
  }
}

9.2 Redux

js 复制代码
// Redux reducer
function reducer(state = initialState, action) {
  switch (action.type) {
    case 'UPDATE_DATA':
      // 深度比较
      if (_.isEqual(state.data, action.data)) {
        return state;  // 相同,不更新
      }
      return { ...state, data: action.data };
    default:
      return state;
  }
}

9.3 缓存(Memoization)

js 复制代码
// 使用深度比较的缓存
function memoize(fn) {
  const cache = new Map();
  return function(...args) {
    const key = JSON.stringify(args);
    
    for (let [cachedKey, cachedValue] of cache) {
      if (isDeepEqual(JSON.parse(cachedKey), args)) {
        return cachedValue;
      }
    }
    
    const result = fn(...args);
    cache.set(key, result);
    return result;
  };
}

10. 最佳实践总结

场景 推荐方法 原因
比较引用是否相同 ===Object.is 最快,最直接
简单对象深度比较 JSON.stringify 简单,但有限制
复杂对象深度比较 Lodash 的 _.isEqual 可靠,功能完整
性能关键,比较第一层 浅比较 最快,适合不可变数据
自定义比较逻辑 自定义比较函数 灵活可控
比较类实例 实现 equals方法 面向对象,语义清晰

选择指南

  1. 引用比较用 ===值比较用深度比较
  2. 性能优先用浅比较精度优先用深度比较
  3. 生产环境用 Lodash简单场景用 JSON.stringify
  4. 特殊对象要特殊处理(Date、RegExp、Map、Set 等)
  5. 注意边界情况(循环引用、Symbol、undefined 等)

根据具体需求选择合适的方法,平衡性能、准确性和可维护性。

相关推荐
前端老宋Running2 小时前
一种名为“Webpack 配置工程师”的已故职业—— Vite 与“零配置”的快乐
前端·vite·前端工程化
用户6600676685392 小时前
从“养猫”看懂JS面向对象:原型链与Class本质拆解
前端·javascript·面试
parade岁月2 小时前
我的第一个 TDesign PR:修复 Empty 组件的 v-if 警告
前端
云鹤_2 小时前
【Amis源码阅读】低代码如何实现交互(下)
前端·低代码·架构
StarkCoder2 小时前
一次搞懂 iOS 组合布局:用 CompositionalLayout 打造马赛克 + 网格瀑布流
前端
dhdjjsjs2 小时前
Day30 Python Study
开发语言·前端·python
T___T2 小时前
通过 MCP 让 AI 读懂你的 Figma 设计稿
前端·人工智能
清妍_2 小时前
踩坑记录:Taro.createSelectorQuery找不到元素
前端
爬山算法2 小时前
Redis(169)如何使用Redis实现数据同步?
前端·redis·bootstrap