How can I determine equality for two JavaScript objects?

Learn how can i determine equality for two javascript objects? with practical examples, diagrams, and best practices. Covers javascript, object, equality development techniques with visual explanat...

Deep Equality in JavaScript: Comparing Objects Effectively

Hero image for How can I determine equality for two JavaScript objects?

Explore various techniques for determining if two JavaScript objects are deeply equal, understanding their limitations and best use cases.

In JavaScript, comparing two objects for equality isn't as straightforward as comparing primitive values like numbers or strings. The == and === operators only check for reference equality, meaning they determine if two variables point to the exact same object in memory, not if their contents are identical. This article delves into the nuances of object equality and provides practical methods for performing deep comparisons.

Understanding Reference vs. Value Equality

Before diving into solutions, it's crucial to grasp the difference between reference and value equality. When you create an object in JavaScript, a unique memory address is allocated for it. Variables then store a reference to this address. The == and === operators compare these references.

For primitive types (strings, numbers, booleans, null, undefined, symbols, BigInt), == and === compare their actual values. However, for objects (including arrays and functions), these operators only return true if both variables refer to the exact same object instance.

const obj1 = { a: 1, b: 'hello' };
const obj2 = { a: 1, b: 'hello' };
const obj3 = obj1;

console.log(obj1 === obj2); // false (different references)
console.log(obj1 === obj3); // true (same reference)

Demonstrating reference equality in JavaScript

Implementing Deep Equality Comparison

To truly compare the contents of two objects, you need a 'deep equality' check. This involves recursively traversing both objects and comparing their properties at each level. This process can become complex, especially when dealing with nested objects, arrays, and different data types. A robust deep equality function must handle various scenarios:

  1. Primitive Values: Directly compare using ===.
  2. Objects/Arrays: Recursively call the deep equality function.
  3. Functions/Symbols: Typically considered unequal unless they are the exact same reference.
  4. null and undefined: Handle explicitly.
  5. Circular References: Prevent infinite loops.
flowchart TD
    A[Start Comparison] --> B{Are A and B strictly equal?};
    B -- Yes --> Z[Return true];
    B -- No --> C{Are A or B null/undefined or not objects?};
    C -- Yes --> D{Return false (unless both null/undefined)};
    C -- No --> E{Are A and B arrays?};
    E -- Yes --> F{Compare lengths};
    F -- Unequal --> D;
    F -- Equal --> G{Iterate and deep compare elements};
    G -- All equal --> Z;
    G -- Any unequal --> D;
    E -- No --> H{Compare number of keys};
    H -- Unequal --> D;
    H -- Equal --> I{Iterate and deep compare properties};
    I -- All equal --> Z;
    I -- Any unequal --> D;

Simplified flowchart for a deep equality comparison algorithm

function deepEqual(obj1, obj2) {
  if (obj1 === obj2) return true;

  if (obj1 == null || typeof obj1 != 'object' ||
      obj2 == null || typeof obj2 != 'object') {
    return false;
  }

  const keys1 = Object.keys(obj1);
  const keys2 = Object.keys(obj2);

  if (keys1.length !== keys2.length) return false;

  for (const key of keys1) {
    if (!keys2.includes(key) || !deepEqual(obj1[key], obj2[key])) {
      return false;
    }
  }

  return true;
}

const a = { x: 1, y: { z: 2 } };
const b = { x: 1, y: { z: 2 } };
const c = { x: 1, y: { z: 3 } };

console.log(deepEqual(a, b)); // true
console.log(deepEqual(a, c)); // false
console.log(deepEqual([1, {a:2}], [1, {a:2}])); // true

A basic recursive deep equality function for JavaScript objects and arrays.

Handling Edge Cases and Performance

A robust deep equality check needs to consider several edge cases:

  • Circular References: Objects that refer back to themselves can cause infinite loops in recursive functions. Solutions often involve keeping track of visited objects.
  • Different Object Types: Comparing a Date object to a plain object, even if their internal values match, should typically return false.
  • NaN Comparison: NaN === NaN is false. A deep equality function should correctly identify NaN values as equal.
  • Performance: Deep equality checks can be computationally expensive, especially for very large or deeply nested objects. For performance-critical applications, consider if a full deep comparison is truly necessary or if a simpler check suffices.

Libraries like Lodash's _.isEqual are highly optimized and handle these complexities, making them a preferred choice for most real-world applications.

// Using Lodash for robust deep equality
import _ from 'lodash';

const objA = { a: 1, b: { c: 2 } };
const objB = { a: 1, b: { c: 2 } };
const objC = { a: 1, b: { c: 3 } };

console.log(_.isEqual(objA, objB)); // true
console.log(_.isEqual(objA, objC)); // false

// Handling NaN
console.log(_.isEqual({ x: NaN }, { x: NaN })); // true (correctly handles NaN)

// Handling circular references (Lodash handles this automatically)
const circularObj1 = {};
const circularObj2 = {};
circularObj1.self = circularObj1;
circularObj2.self = circularObj2;
console.log(_.isEqual(circularObj1, circularObj2)); // true

Example of using Lodash's _.isEqual for comprehensive deep equality.