How can I determine equality for two JavaScript objects?
Categories:
Deep Equality in JavaScript: Comparing Objects Effectively

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
JSON.stringify()
method can be a quick-and-dirty way to compare simple objects, but it has limitations. It fails if object keys are in a different order, if objects contain undefined
values, functions, or Symbol
properties, as these are omitted during stringification.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:
- Primitive Values: Directly compare using
===
. - Objects/Arrays: Recursively call the deep equality function.
- Functions/Symbols: Typically considered unequal unless they are the exact same reference.
null
andundefined
: Handle explicitly.- 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.
deepEqual
function is a simplified example. For production-grade code, consider using well-tested libraries like Lodash's _.isEqual()
which handle edge cases such as circular references, Date
objects, RegExp
objects, and more complex data structures.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 returnfalse
. NaN
Comparison:NaN === NaN
isfalse
. A deep equality function should correctly identifyNaN
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.