5 Different Ways to Deep Compare JavaScript Objects

Summarize this blog post with:

TL;DR: Deep comparison of JavaScript objects is broken down through multiple approaches from native techniques like JSON handling and recursion to utility libraries highlighting real-world use cases, edge cases, and performance trade-offs that impact data consistency, debugging accuracy, and scalable application design.

JavaScript has 7 primitive data types, and comparing the values of any of these types using an equality operator is straightforward. However, comparing non-primitive types such as objects is tricky, the usual equality operators do not compare object values the way most developers expect.

In JavaScript, two objects can look identical but still not be equal, because JavaScript checks if they are the same reference in memory, not whether they contain the same values. If you want to know whether two objects truly have the same content, you need a deep comparison, one that checks every key and every nested value.

This matters in real-world development whenever you work with app state, API responses, form data, or test assertions. Without deep comparison, you may miss real changes, introduce subtle bugs, or write unreliable tests.

In this article, we will discuss five different ways to deep compare JavaScript objects, when to use each method, and the trade-offs to consider.

Two types of equality in JavaScript

Before comparing object values, it helps to understand the two different equality models JavaScript uses.

  • Referential equality: Determines whether two operands refer to the exact same object instance in memory. This can be checked using strict equality (===), coercive equality (==), or Object.is().
  • Deep equality: Determines whether two objects are equal by comparing each property and nested value, regardless of whether they are the same instance.
const a = { x: 1 };
const b = { x: 1 };
const c = a;

console.log(a === b); // false — different references
console.log(a === c); // true  — same reference

Referential equality is simple and fast. Deep equality is what you need when you care about the actual content of two objects.

Why Deep comparison is tricky in JavaScript

Deep comparison sounds straightforward, but objects in JavaScript can store many different kinds of values, including nested objects, arrays, Dates, RegExp, Map, Set, NaN, undefined, and more, and each behaves differently under comparison.

Two objects may look identical but still differ in key order or internal structure. Some objects also have circular references; they refer back to themselves, which can cause naive recursive functions to loop infinitely.

Because of these differences, no single comparison method handles every case correctly. This is why choosing the right approach depends on how simple or complex your data is.

1. Manual comparison

Best for: Known data structures with no external dependencies.

The most direct approach is writing your own recursive function that walks through two values and decides whether they are deeply equal. It gives you full control and no added library weight.

Key rules your Comparator should handle

  • Primitives: Use Object.is() for correct NaN and signed-zero behavior.
  • Date: Compare a.getTime() === b.getTime().
  • RegExp: Compare source and flags.
  • Arrays / Plain objects: Compare lengths or key counts, then recurse member by member.
  • Map / Set: Both iterate in insertion order by spec; decide whether to compare as ordered entries or unordered membership.
  • Circular references: Maintain a WeakMap from object A to object B to prevent infinite recursion.

Example

function isObject(x) {
  return x !== null && typeof x === 'object';
}

function deepEqual(a, b) {
  if (Object.is(a, b)) return true; // handles NaN and -0 correctly
  if (!isObject(a) || !isObject(b)) return false;

  const aKeys = Object.keys(a);
  const bKeys = Object.keys(b);

  if (aKeys.length !== bKeys.length) return false;

  for (const key of aKeys) {
    if (!Object.prototype.hasOwnProperty.call(b, key)) return false;
    if (!deepEqual(a[key], b[key])) return false;
  }

  return true;
}

// Example
deepEqual({ x: 1, y: [2, 3] }, { y: [2, 3], x: 1 }); // true

Note: This version handles plain objects and arrays with key-order independence. For production use, add cases for Date, RegExp, Map, Set, and a WeakMap to track visited nodes for circular reference protection.

When to use

  • You know the structure of your data (plain objects or arrays) and want to avoid external dependencies.
  • In performance-sensitive code paths where a custom comparator tailored to your exact data shape can be faster than a general-purpose library.

Limitations

  • Easy to miss edge cases: circular references, Date, RegExp, Map, Set, ArrayBuffer, TypedArray.
  • More code to write, maintain, and test yourself.
  • Special values like NaN, -0, symbol keys, non-enumerable properties, and prototype differences require explicit handling if they matter to your use case.

2. JSON.stringify()

Best for: Quick checks on simple, JSON-safe objects with consistent key order.

JSON.stringify() is a quick shortcut: convert both objects to JSON strings and compare the strings. Since JavaScript can easily compare two strings with ===, this works as a basic deep equality check for simple data.

const person1 = { firstName: "John", lastName: "Doe", age: 35 };
const person2 = { firstName: "John", lastName: "Doe", age: 35 };

JSON.stringify(person1) === JSON.stringify(person2); // true

The Key Order problem

This method has one critical downside: the order of keys matters. Take the same objects but reorder the keys in person2:

const person2 = {
  age: 35,
  firstName: "John",
  lastName: "Doe"
};

console.log(JSON.stringify(person1) === JSON.stringify(person2)); 
// false — same data, different order!

The values have not changed, but the strings differ because the key order differs. This will silently return false for equal objects.

When to use

  • Quick checks in small scripts or tests where data is simple, JSON-friendly, has no circular references, and key order is guaranteed to be consistent.
  • Basic logging or rough comparison of simple configuration objects.

Limitations

  • Silent data loss: undefined, functions, and Symbol values are silently dropped during serialization, which can cause two different objects to appear equal.
  • Key order sensitivity: Different key orders produce different strings, giving incorrect results.
  • Throws on unsupported types: Circular references or BigInt values throw a runtime error with no warning.

3. Lodash _.isEqual / _.isEqualWith

Best for: General application use where a reliable, production-ready comparison is needed.

The Lodash library provides _.isEqual(), a deep comparison function that returns true when two values have the same content. It compares step by step and uses strict equality (===) on all leaf nodes.

It supports a wide range of built-in JavaScript types automatically: Arrays, ArrayBuffer, Date, Map, Set, TypedArray, Error, RegExp, Symbol, and plain objects. When comparing objects, it only checks its own enumerable properties, not anything inherited from prototypes.

import isEqual from "lodash/isEqual";
// or: const isEqual = require("lodash/isEqual");

const person1 = { firstName: "John", lastName: "Doe", age: 35 };
const person2 = { firstName: "John", lastName: "Doe", age: 35 };

console.log(isEqual(person1, person2)); // true

This method handles arrays, array buffers, date objects, and more. It is also available as a separate npm module (lodash.isequal), so you don’t have to import the full library.

When to use

  • In real applications, where you need an accurate comparison across many common data types.
  • When your project already includes Lodash, no additional bundle cost.
  • When you want a reliable, battle-tested comparison method without writing and maintaining your own.

Limitations

  • Adds an external dependency (though lodash-es and tree-shaking can minimize the bundle impact).
  • May be slightly slower than a custom function built specifically for your data shape.
  • Comparison rules are fixed; for custom behavior (e.g., ignoring certain fields), you need _.isEqualWith

4. deep-equal Library

Best for: Lightweight deep comparison outside of Lodash, with configurable strict or loose modes.

The deep-equal library is a focused npm package with over 11 million weekly downloads. It mirrors the behavior of Node.js’s assert.deepEqual(), but as a standalone tool you can use in any environment, whether browser or server.

deepEqual(a, b) takes an optional third options parameter that controls whether to use strict (===) or coercive (==) equality on leaf nodes. The default is coercive equality.

const deepEqual = require("deep-equal");

const person1 = { firstName: "John", lastName: "Doe", age: 35 };
const person2 = { firstName: "John", lastName: "Doe", age: "35" }; // age is a string here

deepEqual(person1, person2); // true — coercive: 35 == "35"
deepEqual(person1, person2, { strict: true }); // false — strict: 35 !== "35"

When person2 uses the same type as person1:

const person2 = { firstName: "John", lastName: "Doe", age: 35 };
deepEqual(person1, person2, { strict: true }); // true

When to use

  • You want a small, focused deep comparison tool without pulling in all of Lodash.
  • You need configurable strict vs. loose comparison behavior.
  • You’re working in client or server environments where keeping the bundle lean matters.

Limitations

  • Feature set varies between versions; test how it handles Map, Set, typed arrays, and circular references before committing to it for complex data.
  • Fewer helper functions than Lodash; no equivalent to _.isEqualWith for custom comparison logic.

5. Framework-Specific and Platform-Specific methods

Best for: When you are already working inside a specific environment and want to use its native tools.

Deep equality is built into many JavaScript tools and platforms. Rather than adding a library, you can reach for the comparison method that belongs to the environment you are already working in.

Node.js: assert.deepStrictEqual

Node.js provides assert.deepStrictEqual(actual, expected) as part of its built-in assert module. It checks deep equality using strict rules (===) on all leaf nodes. If the values are equal, it returns without error. If they are not, it throws an AssertionError with a helpful diff showing exactly what failed.

const a = { x: 1 };
const b = { x: 1 };
const c = a;

console.log(a === b); // false — different references
console.log(a === c); // true — same reference

For general-purpose deep equality checks outside of assertions, use util.isDeepStrictEqual(a, b) from Node’s util module; it returns a boolean instead of throwing.

AngularJS (1.x): angular.equals

The Angular library provides angular.equals(obj1, obj2) under the ng module. It compares two values deeply using strict equality and returns a boolean.

Special rules unique to AngularJS:

  • NaN is treated as equal to NaN (unlike standard JavaScript where NaN !== NaN).
  • Properties starting with $ are ignored, these are internal AngularJS framework fields.
  • Function properties are ignored, two objects can be considered equal even if their function properties differ.
var person1 = { firstName: "John", lastName: "Doe", age: 35 };
var person2 = { firstName: "John", lastName: "Doe", age: 35 };

console.log(angular.equals(person1, person2)); // true

Jest / Vitest: expect(a).toEqual(b)

In testing environments, use your test framework’s built-in equality matcher. Jest and Vitest’s toEqual performs deep equality and outputs a clear diff when the assertion fails, which is far more readable than a thrown error.

expect(person1).toEqual(person2); // passes; shows diff on failure

When to use

  • You are already working inside that environment, such as a Node.js server, Jest tests, or Angular app, and want to follow the platform’s recommended practices.
  • You want zero additional dependencies.

Limitations

  • Tied to their environment, not easily portable across platforms.
  • Fixed comparison rules; limited customization of equality behavior.
  • Some methods (like toEqual) are designed for testing, not general-purpose application logic.

Choosing the right method

ScenarioRecommended MethodReason
Simple, JSON-safe data with consistent key orderJSON.stringifyQuick and zero dependencies; acceptable for simple scripts
General app use, most data typesLodash _.isEqualReliable, handles many types, production-tested
Lightweight alternative to Lodashdeep-equalFocused tool; test with your specific data types first
Node.js server codeutil.isDeepStrictEqualBuilt-in; no dependency; strict by default
Test assertionstoEqual (Jest/Vitest)Readable failure diffs; designed for this exact use case
Performance-critical + known object shapeManual comparatorTailored to your schema; handle only the types you need

Frequently Asked Questions

How do you deep compare two JavaScript objects?

You can deep compare JavaScript objects using manual recursion, JSON.stringify(), Lodash’s _.isEqual(), the deep-equal npm package, or platform-specific methods like assert.deepStrictEqual in Node.js. The right choice depends on your data complexity and environment.

What is the difference between referential equality and deep equality?

Referential equality (===) checks if two variables point to the same object in memory. Deep equality checks if two objects contain the same values, even if they are different instances.

Why does JSON.stringify sometimes give wrong results?

JSON.stringify is sensitive to key order; the same object with keys in a different order will produce a different string. It also silently drops undefined, functions, and Symbol values, and throws on circular references or BigInt.

Is Lodash _.isEqual safe for production?

Yes. _.isEqual is mature, widely adopted, handles circular references, and supports a broad range of JavaScript types including Date, Map, Set, RegExp, and TypedArray.

How do I deep compare objects in Node.js without a library?

Use util.isDeepStrictEqual(a, b) from Node’s built-in util module. It returns a boolean and requires no external dependencies.

Conclusion

JavaScript deep object comparison requires careful handling because objects can contain many different value types, and simple equality operators only check references. As discussed, there are five practical methods: manual comparison, JSON.stringify, Lodash _.isEqual, the deep-equal library, and framework-specific tools, and each works best in different situations.

For most production applications, _.isEqual is the safest default. For testing, use toEqual. For Node.js, use util.isDeepStrictEqual. For performance-critical paths with known data shapes, a manual comparator gives you the most control.

Choose the method that fits your data complexity, environment, and long-term maintenance needs so your code will be cleaner, safer, and more reliable.

Be the first to get updates

Mahesh SamarasingheMahesh Samarasinghe profile icon

Meet the Author

Mahesh Samarasinghe

I am a full-stack software engineer with over two years of experience working in the MERN stack and AWS. I am also an AWS Community Builder and a content writer in multiple platforms.

Leave a comment