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 等)

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

相关推荐
烤麻辣烫1 天前
Web开发概述
前端·javascript·css·vue.js·html
Front思1 天前
Vue3仿美团实现骑手路线规划
开发语言·前端·javascript
徐同保1 天前
Nano Banana AI 绘画创作前端代码(使用claude code编写)
前端
Ulyanov1 天前
PyVista与Tkinter桌面级3D可视化应用实战
开发语言·前端·python·3d·信息可视化·tkinter·gui开发
计算机程序设计小李同学1 天前
基于Web和Android的漫画阅读平台
java·前端·vue.js·spring boot·后端·uniapp
干前端1 天前
Message组件和Vue3 进阶:手动挂载组件与 Diff 算法深度解析
javascript·vue.js·算法
lkbhua莱克瓦241 天前
HTML与CSS核心概念详解
前端·笔记·html·javaweb
沛沛老爹1 天前
从Web到AI:Agent Skills CI/CD流水线集成实战指南
java·前端·人工智能·ci/cd·架构·llama·rag
和你一起去月球1 天前
动手学Agent应用开发(TS/JS 最简实践指南)
开发语言·javascript·ecmascript·agent·mcp
GISer_Jing1 天前
1.17-1.23日博客之星投票,每日可投
前端·人工智能·arcgis