Skip to main content

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

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 references
console.log(obj1 === obj3); // true - same reference

Even 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); // false
console.log(arr1 === arr1); // true

Why 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 examples
const user1 = { name: "John", age: 30 };
const user2 = { name: "John", age: 30 };
const user3 = { name: "John", age: 31 };
console.log(shallowEqual(user1, user2)); // true
console.log(shallowEqual(user1, user3)); // false

Enhanced 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 reference

Deep 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;
}
// Usage
const 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)); // true

Advanced 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 cases
const obj1 = { name: "John", age: 30 };
const obj2 = { name: "John", age: 30 };
console.log(jsonEqual(obj1, obj2)); // true

Problems with JSON.stringify

Anti-pattern: JSON.stringify has several issues:

// Problem 1: Property order matters
const 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 omitted
const obj3 = { a: 1, b: undefined };
const obj4 = { a: 1 };
console.log(jsonEqual(obj3, obj4)); // true - but they're not equal!
// Problem 3: Functions are omitted
const obj5 = { a: 1, fn: () => {} };
const obj6 = { a: 1 };
console.log(jsonEqual(obj5, obj6)); // true - but they're not equal!
// Problem 4: Dates become strings
const 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 errors
const circular = { a: 1 };
circular.self = circular;
// jsonEqual(circular, circular); // TypeError: Converting circular structure to JSON

When 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 examples
console.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") }),
); // true

Using 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)); // true

Benefits:

  • ✅ Handles edge cases (circular refs, Dates, RegExp, etc.)
  • ✅ Well-tested and maintained
  • ✅ Optimized for performance
  • ✅ Supports various data types

Installation:

Terminal window
pnpm add lodash
pnpm add -D @types/lodash

Ramda 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)); // true

fast-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)); // true

Installation:

Terminal window
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

MethodSpeedUse Case
=== (reference)FastestWhen you know objects share the same reference
Shallow equalityFastFlat objects, React props comparison
JSON.stringifyMediumSimple objects, no edge cases
Deep equalitySlowComplex nested structures
Library (lodash)Medium-SlowProduction 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 itself
console.log(NaN === NaN); // false
// Your deep equality function should handle this
function handlesNaN(obj1, obj2) {
if (Number.isNaN(obj1) && Number.isNaN(obj2)) {
return true;
}
return obj1 === obj2;
}

Negative Zero

// -0 and +0 are considered equal
console.log(-0 === +0); // true
console.log(Object.is(-0, +0)); // false
// Use Object.is for strict equality
function 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 symbols
const 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 properties
console.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); // false
console.log(deepEqual(arr1, arr2)); // Need special handling for TypedArrays

Map 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 Set
function 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 props
function MyComponent({ user }) {
const prevUser = useRef();
if (!shallowEqual(prevUser.current, user)) {
// User changed
prevUser.current = user;
}
}
// ✅ Deep equality for complex state
function 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 paths
function renderComponent(props) {
// This runs on every render - too expensive!
if (deepEqual(props, previousProps)) {
return cachedResult;
}
}
// ✅ Use shallow equality or memoization
const 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 cases
describe("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.