How to Check if Two Objects Are Equal in JavaScript
Learn the different methods to compare objects in JavaScript, from shallow to deep equality checks, with practical examples and performance considerations.
Table of Contents
- Introduction
- The Problem with Object Comparison
- Shallow Equality Comparison
- Deep Equality Comparison
- Using JSON.stringify for Comparison
- Custom Deep Equality Function
- Using Third-Party Libraries
- Performance Considerations
- Edge Cases and Gotchas
- Best Practices
- Conclusion
Introduction
Comparing objects in JavaScript is one of those tasks that seems simple at first glance but quickly reveals its complexity. Unlike primitive values (strings, numbers, booleans), objects are compared by reference, not by value. This means that even if two objects have identical properties and values, === will return false because they’re different object instances in memory.
Understanding how to properly check object equality is crucial for writing robust JavaScript code. Whether you’re building a state management system, implementing caching logic, or writing unit tests, you’ll need reliable object comparison methods. In this guide, we’ll explore various approaches to object equality checking, from simple shallow comparisons to comprehensive deep equality functions, along with their trade-offs and use cases.
The Problem with Object Comparison
When you use == or === to compare objects in JavaScript, you’re comparing their references in memory, not their actual content. This fundamental behavior often catches developers off guard.
const obj1 = { name: "John", age: 30 };const obj2 = { name: "John", age: 30 };const obj3 = obj1;
console.log(obj1 === obj2); // false - different referencesconsole.log(obj1 === obj3); // true - same referenceEven though obj1 and obj2 contain identical properties and values, they’re stored at different memory locations, so the strict equality operator returns false. Only obj1 === obj3 returns true because obj3 points to the same object instance as obj1.
This reference-based comparison extends to arrays as well:
const arr1 = [1, 2, 3];const arr2 = [1, 2, 3];
console.log(arr1 === arr2); // falseconsole.log(arr1 === arr1); // trueWhy This Matters
Understanding reference equality is crucial because:
- State Management: When checking if state has changed in React or other frameworks
- Caching: Determining if cached data matches new data
- Testing: Asserting that objects match expected values
- Data Validation: Comparing form data or API responses
Shallow Equality Comparison
A shallow equality check compares the top-level properties of objects. It doesn’t recursively check nested objects or arrays. This is faster than deep equality but has limitations.
Manual Shallow Comparison
You can manually check each property:
function shallowEqual(obj1, obj2) { // Check if both are null or undefined if (obj1 == null || obj2 == null) { return obj1 === obj2; }
// Get keys from both objects const keys1 = Object.keys(obj1); const keys2 = Object.keys(obj2);
// Different number of keys means not equal if (keys1.length !== keys2.length) { return false; }
// Check each key-value pair for (let key of keys1) { if (obj1[key] !== obj2[key]) { return false; } }
return true;}
// Usage examplesconst user1 = { name: "John", age: 30 };const user2 = { name: "John", age: 30 };const user3 = { name: "John", age: 31 };
console.log(shallowEqual(user1, user2)); // trueconsole.log(shallowEqual(user1, user3)); // falseEnhanced Shallow Comparison
A more robust version handles edge cases:
function shallowEqual(obj1, obj2) { // Handle null/undefined cases if (obj1 === obj2) { return true; }
if (obj1 == null || obj2 == null) { return false; }
// Check if both are objects if (typeof obj1 !== "object" || typeof obj2 !== "object") { return false; }
const keys1 = Object.keys(obj1); const keys2 = Object.keys(obj2);
if (keys1.length !== keys2.length) { return false; }
// Check each property for (let key of keys1) { if (!keys2.includes(key) || obj1[key] !== obj2[key]) { return false; } }
return true;}Limitations of Shallow Equality
⚠️ Important: Shallow equality fails with nested objects:
const obj1 = { user: { name: "John", age: 30 } };const obj2 = { user: { name: "John", age: 30 } };
console.log(shallowEqual(obj1, obj2)); // false - nested objects compared by referenceDeep Equality Comparison
Deep equality recursively compares all properties, including nested objects and arrays. This is more comprehensive but computationally expensive.
Basic Deep Equality Implementation
Here’s a straightforward deep equality function:
function deepEqual(obj1, obj2) { // Primitive comparison if (obj1 === obj2) { return true; }
// Handle null/undefined if (obj1 == null || obj2 == null) { return false; }
// Check if both are objects if (typeof obj1 !== "object" || typeof obj2 !== "object") { return false; }
// Handle arrays if (Array.isArray(obj1) !== Array.isArray(obj2)) { return false; }
const keys1 = Object.keys(obj1); const keys2 = Object.keys(obj2);
if (keys1.length !== keys2.length) { return false; }
// Recursively check each property for (let key of keys1) { if (!keys2.includes(key)) { return false; }
if (!deepEqual(obj1[key], obj2[key])) { return false; } }
return true;}
// Usageconst obj1 = { name: "John", address: { city: "New York", zip: "10001", }, hobbies: ["reading", "coding"],};
const obj2 = { name: "John", address: { city: "New York", zip: "10001", }, hobbies: ["reading", "coding"],};
console.log(deepEqual(obj1, obj2)); // trueAdvanced Deep Equality with Circular Reference Handling
For production use, you need to handle circular references:
function deepEqual(obj1, obj2, visited = new WeakSet()) { // Primitive comparison if (obj1 === obj2) { return true; }
// Handle null/undefined if (obj1 == null || obj2 == null) { return false; }
// Check if both are objects if (typeof obj1 !== "object" || typeof obj2 !== "object") { return false; }
// Handle circular references if (visited.has(obj1) || visited.has(obj2)) { return obj1 === obj2; } visited.add(obj1); visited.add(obj2);
// Handle arrays if (Array.isArray(obj1) !== Array.isArray(obj2)) { return false; }
const keys1 = Object.keys(obj1); const keys2 = Object.keys(obj2);
if (keys1.length !== keys2.length) { return false; }
// Recursively check each property for (let key of keys1) { if (!keys2.includes(key)) { return false; }
if (!deepEqual(obj1[key], obj2[key], visited)) { return false; } }
return true;}Using JSON.stringify for Comparison
One popular approach is to serialize both objects to JSON strings and compare them. This is simple but has significant limitations.
Basic JSON.stringify Comparison
function jsonEqual(obj1, obj2) { return JSON.stringify(obj1) === JSON.stringify(obj2);}
// Works for simple casesconst obj1 = { name: "John", age: 30 };const obj2 = { name: "John", age: 30 };
console.log(jsonEqual(obj1, obj2)); // trueProblems with JSON.stringify
❌ Anti-pattern: JSON.stringify has several issues:
// Problem 1: Property order mattersconst obj1 = { a: 1, b: 2 };const obj2 = { b: 2, a: 1 };console.log(jsonEqual(obj1, obj2)); // false - even though objects are equal!
// Problem 2: Undefined values are omittedconst obj3 = { a: 1, b: undefined };const obj4 = { a: 1 };console.log(jsonEqual(obj3, obj4)); // true - but they're not equal!
// Problem 3: Functions are omittedconst obj5 = { a: 1, fn: () => {} };const obj6 = { a: 1 };console.log(jsonEqual(obj5, obj6)); // true - but they're not equal!
// Problem 4: Dates become stringsconst obj7 = { date: new Date("2024-01-01") };const obj8 = { date: "2024-01-01T00:00:00.000Z" };console.log(jsonEqual(obj7, obj8)); // false - dates serialized differently
// Problem 5: Circular references cause errorsconst circular = { a: 1 };circular.self = circular;// jsonEqual(circular, circular); // TypeError: Converting circular structure to JSONWhen JSON.stringify Works
✅ Use JSON.stringify when:
- You control the object structure
- Property order is consistent
- No functions, undefined values, or Dates
- No circular references
- You need a quick, simple solution for simple objects
Custom Deep Equality Function
For production applications, a custom deep equality function that handles edge cases is often the best solution.
Comprehensive Deep Equality Implementation
function deepEqual(obj1, obj2, visited = new WeakMap()) { // Same reference check if (obj1 === obj2) { return true; }
// Handle null/undefined if (obj1 == null || obj2 == null) { return obj1 === obj2; }
// Type check const type1 = typeof obj1; const type2 = typeof obj2;
if (type1 !== type2) { return false; }
// Handle primitives if (type1 !== "object") { // Handle NaN if (Number.isNaN(obj1) && Number.isNaN(obj2)) { return true; } return obj1 === obj2; }
// Handle circular references if (visited.has(obj1) && visited.get(obj1) === obj2) { return true; } visited.set(obj1, obj2);
// Handle arrays if (Array.isArray(obj1)) { if (!Array.isArray(obj2) || obj1.length !== obj2.length) { return false; } for (let i = 0; i < obj1.length; i++) { if (!deepEqual(obj1[i], obj2[i], visited)) { return false; } } return true; }
// Handle Date objects if (obj1 instanceof Date && obj2 instanceof Date) { return obj1.getTime() === obj2.getTime(); }
// Handle RegExp objects if (obj1 instanceof RegExp && obj2 instanceof RegExp) { return obj1.toString() === obj2.toString(); }
// Handle objects const keys1 = Object.keys(obj1); const keys2 = Object.keys(obj2);
if (keys1.length !== keys2.length) { return false; }
for (let key of keys1) { if (!keys2.includes(key)) { return false; } if (!deepEqual(obj1[key], obj2[key], visited)) { return false; } }
return true;}
// Usage examplesconsole.log(deepEqual({ a: 1, b: { c: 2 } }, { a: 1, b: { c: 2 } })); // true
console.log(deepEqual([1, 2, { a: 3 }], [1, 2, { a: 3 }])); // true
console.log( deepEqual({ date: new Date("2024-01-01") }, { date: new Date("2024-01-01") }),); // trueUsing Third-Party Libraries
For many projects, using a well-tested library is preferable to maintaining custom code.
Lodash isEqual
Lodash provides a robust isEqual function:
import { isEqual } from "lodash";
const obj1 = { a: 1, b: { c: 2 } };const obj2 = { a: 1, b: { c: 2 } };
console.log(isEqual(obj1, obj2)); // trueBenefits:
- ✅ Handles edge cases (circular refs, Dates, RegExp, etc.)
- ✅ Well-tested and maintained
- ✅ Optimized for performance
- ✅ Supports various data types
Installation:
pnpm add lodashpnpm add -D @types/lodashRamda equals
Ramda provides a functional approach:
import { equals } from "ramda";
const obj1 = { a: 1, b: { c: 2 } };const obj2 = { a: 1, b: { c: 2 } };
console.log(equals(obj1, obj2)); // truefast-deep-equal
A lightweight, fast alternative:
import equal from "fast-deep-equal";
const obj1 = { a: 1, b: { c: 2 } };const obj2 = { a: 1, b: { c: 2 } };
console.log(equal(obj1, obj2)); // trueInstallation:
pnpm add fast-deep-equal💡 Tip: Choose fast-deep-equal if bundle size matters, or lodash if you need comprehensive edge case handling.
Performance Considerations
Different comparison methods have varying performance characteristics. Understanding these helps you choose the right approach.
Performance Comparison
| Method | Speed | Use Case |
|---|---|---|
=== (reference) | Fastest | When you know objects share the same reference |
| Shallow equality | Fast | Flat objects, React props comparison |
| JSON.stringify | Medium | Simple objects, no edge cases |
| Deep equality | Slow | Complex nested structures |
| Library (lodash) | Medium-Slow | Production apps needing reliability |
Optimizing Deep Equality
You can optimize deep equality checks:
function optimizedDeepEqual(obj1, obj2, visited = new WeakMap(), depth = 0) { // Early exit for same reference if (obj1 === obj2) return true;
// Limit recursion depth (optional safety measure) if (depth > 100) { console.warn("Deep equality check exceeded depth limit"); return false; }
// Handle null/undefined if (obj1 == null || obj2 == null) return obj1 === obj2;
// Type check if (typeof obj1 !== "object" || typeof obj2 !== "object") { return obj1 === obj2; }
// Circular reference check if (visited.has(obj1)) return visited.get(obj1) === obj2; visited.set(obj1, obj2);
// Array handling if (Array.isArray(obj1)) { if (!Array.isArray(obj2) || obj1.length !== obj2.length) { return false; } for (let i = 0; i < obj1.length; i++) { if (!optimizedDeepEqual(obj1[i], obj2[i], visited, depth + 1)) { return false; } } return true; }
// Object handling const keys1 = Object.keys(obj1); const keys2 = Object.keys(obj2);
if (keys1.length !== keys2.length) return false;
for (let key of keys1) { if (!keys2.includes(key)) return false; if (!optimizedDeepEqual(obj1[key], obj2[key], visited, depth + 1)) { return false; } }
return true;}When to Use Each Method
✅ Use shallow equality when:
- Comparing React props or state
- Objects are flat (no nesting)
- Performance is critical
- You only care about top-level changes
✅ Use deep equality when:
- Objects have nested structures
- You need comprehensive comparison
- Accuracy is more important than speed
- Working with complex data structures
Edge Cases and Gotchas
Object equality checking has many edge cases that can trip you up. Here are the most common ones:
NaN Comparison
// NaN is not equal to itselfconsole.log(NaN === NaN); // false
// Your deep equality function should handle thisfunction handlesNaN(obj1, obj2) { if (Number.isNaN(obj1) && Number.isNaN(obj2)) { return true; } return obj1 === obj2;}Negative Zero
// -0 and +0 are considered equalconsole.log(-0 === +0); // trueconsole.log(Object.is(-0, +0)); // false
// Use Object.is for strict equalityfunction strictEqual(obj1, obj2) { return Object.is(obj1, obj2);}Symbol Properties
const sym = Symbol("key");const obj1 = { [sym]: "value", a: 1 };const obj2 = { [sym]: "value", a: 1 };
// Object.keys() doesn't include symbolsconst keys1 = Object.keys(obj1); // ['a']const keys2 = Object.keys(obj2); // ['a']
// Use Object.getOwnPropertySymbols() or Reflect.ownKeys()function includesSymbols(obj1, obj2) { const keys1 = Reflect.ownKeys(obj1); const keys2 = Reflect.ownKeys(obj2); // ... comparison logic}Prototype Properties
function Parent() { this.parentProp = "value";}
function Child() { this.childProp = "value";}
Child.prototype = new Parent();
const obj1 = new Child();const obj2 = new Child();
// Object.keys() only gets own propertiesconsole.log(Object.keys(obj1)); // ['childProp']
// For prototype properties, use for...in or Object.getOwnPropertyNames()Typed Arrays
const arr1 = new Uint8Array([1, 2, 3]);const arr2 = new Uint8Array([1, 2, 3]);
console.log(arr1 === arr2); // falseconsole.log(deepEqual(arr1, arr2)); // Need special handling for TypedArraysMap and Set Objects
const map1 = new Map([ ["a", 1], ["b", 2],]);const map2 = new Map([ ["a", 1], ["b", 2],]);
console.log(map1 === map2); // false
// Need special handling for Map and Setfunction handlesMapSet(obj1, obj2) { if (obj1 instanceof Map && obj2 instanceof Map) { if (obj1.size !== obj2.size) return false; for (let [key, value] of obj1) { if (!obj2.has(key) || !deepEqual(value, obj2.get(key))) { return false; } } return true; } // ... rest of comparison}Best Practices
Follow these best practices when implementing object equality checks:
1. Choose the Right Method for Your Use Case
// ✅ Shallow equality for React propsfunction MyComponent({ user }) { const prevUser = useRef();
if (!shallowEqual(prevUser.current, user)) { // User changed prevUser.current = user; }}
// ✅ Deep equality for complex statefunction hasStateChanged(oldState, newState) { return !deepEqual(oldState, newState);}2. Handle Edge Cases Explicitly
function robustDeepEqual(obj1, obj2) { // Handle null/undefined if (obj1 == null || obj2 == null) { return obj1 === obj2; }
// Handle different types if (typeof obj1 !== typeof obj2) { return false; }
// Handle special objects (Date, RegExp, etc.) if (obj1 instanceof Date && obj2 instanceof Date) { return obj1.getTime() === obj2.getTime(); }
// ... rest of implementation}3. Use Libraries for Production Code
💡 Tip: Unless you have specific requirements, use a well-tested library like Lodash’s isEqual for production code. It handles edge cases you might not think of.
4. Consider Performance Impact
// ❌ Don't use deep equality in hot pathsfunction renderComponent(props) { // This runs on every render - too expensive! if (deepEqual(props, previousProps)) { return cachedResult; }}
// ✅ Use shallow equality or memoizationconst memoizedComponent = React.memo(Component, shallowEqual);5. Document Your Comparison Logic
/** * Compares two objects for deep equality. * * Handles: * - Nested objects and arrays * - Circular references * - Date and RegExp objects * - NaN values * * Does NOT handle: * - Functions (compared by reference) * - Symbol properties * - TypedArrays * * @param {any} obj1 - First object to compare * @param {any} obj2 - Second object to compare * @returns {boolean} True if objects are deeply equal */function deepEqual(obj1, obj2) { // ... implementation}6. Test Your Implementation
// Unit tests for edge casesdescribe("deepEqual", () => { it("handles circular references", () => { const obj = { a: 1 }; obj.self = obj; expect(deepEqual(obj, obj)).toBe(true); });
it("handles NaN", () => { expect(deepEqual({ value: NaN }, { value: NaN })).toBe(true); });
it("handles Dates", () => { const date1 = new Date("2024-01-01"); const date2 = new Date("2024-01-01"); expect(deepEqual({ date: date1 }, { date: date2 })).toBe(true); });});Conclusion
Checking object equality in JavaScript is more complex than it first appears. The right approach depends on your specific needs:
- Reference equality (
===): Fastest, but only works for same object instances - Shallow equality: Good for flat objects and React props comparison
- Deep equality: Comprehensive but slower, handles nested structures
- JSON.stringify: Simple but has many limitations
- Libraries: Best for production code, handle edge cases well
Remember to consider performance implications, especially in frequently called code paths. For most production applications, using a well-tested library like Lodash’s isEqual is the safest choice, as it handles edge cases you might not anticipate.
When implementing your own solution, make sure to handle circular references, special objects (Date, RegExp), NaN values, and other edge cases. Test thoroughly with various data structures to ensure your implementation works correctly.
For more JavaScript tips and techniques, check out our JavaScript Array Methods cheatsheet and JavaScript Object Methods cheatsheet. If you’re working with TypeScript, our TypeScript cheatsheet covers type-safe object comparisons as well.