How to perform deep equality checks in JavaScript

Oct 11, 2023#javascript#how-to

Deep equality is a concept in JavaScript that involves comparing complex data structures, such as arrays and objects, to determine if their contents are equal. It goes beyond simple value comparisons and checks whether the elements within nested structures are also equal.

To achieve deep equality, you need to recursively traverse the data structures and compare their elements. Deep equality is useful when you want to ensure that the entire structure, including nested objects and arrays, is identical.

Deep equality is different from following equality checks in JavaScript:

  • Loose equality (==) checks if two values are equal after type coercion.
  • Strict equality (===) checks if two values are equal and requires both the value and data type to be identical. Unlike loose equality, strict equality does not perform type coercion, making it a safer choice for most value comparisons.
  • Object.is() is similar to === in most respects, but with two notable differences related to NaN and -0.
// Comparing NaN
console.log(NaN == NaN);   // false
console.log(NaN === NaN);  // false
console.log(Object.is(NaN, NaN));  // true

// Comparing -0 and 0
console.log(-0 == 0);   // true
console.log(-0 === 0);  // true
console.log(Object.is(-0, 0));  // false

When you compare reference types (arrays, objects, dates, etc.) using ==, ===, or Object.is(), you’re comparing the memory addresses (references) of the objects, not the contents of the objects themselves.

If you want to compare the contents of objects or arrays, you would need to implement a custom deep equality function or use a library like Lodash’s _.isEqual() to perform such comparisons.

Using _.isEqual() of Lodash

To use Lodash to perform deep equality, you can use the _.isEqual() method, which performs a deep comparison between two values to determine if they are equivalent. This method supports comparing arrays, objects, and many other types of values, such as numbers, strings, symbols, etc.

var x = [{a:1, b:2}, {c:3, d:4}];
var y = [{b:2, a:1}, {d:4, c:3}];
_.isEqual(x, y); // => true

If you change the value of any property in either array, the value of d in the second object of y is different from the value of d in the second object of x, it will return false:

var x = [{a:1, b:2}, {c:3, d:4}];
var y = [{b:2, a:1}, {d:5, c:3}];
_.isEqual(x, y); // => false

Using a library like Lodash can save you the effort of writing complex deep equality comparison logic from scratch. It’s a well-tested and reliable solution for comparing complex data structures.

Using a custom deep equality function

Writing your own comparison function can be useful when you want to have more control over how the objects are compared, or when you don’t want to rely on external libraries or methods. But it is very challenging, as you need to consider various cases and scenarios, such as:

  • The objects may have different types, such as primitive values, arrays, functions, dates, etc.
  • The objects may have different structures, such as nested objects, circular references, prototype chains, etc.
  • The objects may have different properties, such as enumerable or non-enumerable, own or inherited, configurable or non-configurable, etc.
  • The objects may have different values, such as NaN, -0, +0, null, undefined, etc.

This function can be complex, especially when dealing with nested arrays and objects, as you’ve noticed. If you find it challenging and want a simpler solution, you can consider using an established library like Lodash or a utility function provided by your JavaScript framework or library of choice.

Using JSON.stringify

You can use JSON.stringify as a technique to perform a deep equality check on objects, but it comes with some caveats. Here’s how you can use JSON.stringify:

function deepEqualWithJSON(obj1, obj2) {
  return JSON.stringify(obj1) === JSON.stringify(obj2);
}

// Example usage
const objA = {
  name: 'John',
  age: 30,
  address: {
    city: 'New York',
    zip: '10001',
  },
};

const objB = {
  name: 'John',
  age: 30,
  address: {
    city: 'New York',
    zip: '10001',
  },
};

const objC = {
  name: 'Alice',
  age: 25,
  address: {
    city: 'Los Angeles',
    zip: '90001',
  }
};

console.log(deepEqualWithJSON(objA, objB)); // true (the contents are the same)
console.log(deepEqualWithJSON(objA, objC)); // false (different values)

This approach serializes the objects into JSON strings and then compares the strings for equality. It can work for simple cases, but there are important limitations and considerations:

  • JSON.stringify does not preserve functions, so if your objects contain functions, those functions will not be considered in the comparison.
  • NaN values will be treated as equal in the comparison, which may not always be desired.
  • Serializing and deserializing large objects with JSON.stringify and JSON.parse can have performance implications, especially for deeply nested objects or large arrays.
  • The values may have different types but the same JSON representation.
  • The values may have circular references that cause an error when stringified.
  • The order of the properties in the objects may affect the comparison.

Using deepEqual() and deepStrictEqual() in Node.js

In Node.js, the assert module provides various functions for performing assertions and tests in your code. Two of the assertion methods in this module are assert.deepEqual() and assert.deepStrictEqual(), which are used for deep equality checks.

These methods allow you to compare complex data structures like objects and arrays to ensure that their contents are equal. The difference between them lies in how they handle strict equality:

  • deepEqual() performs a deep comparison but allows type coercion, so values that can be implicitly converted are considered equal.
  • deepStrictEqual() requires that not only the content but also the data types are identical. It does not allow type coercion, so values with different types are considered unequal.

Here is an original example for assert.deepStrictEqual() method in Node.js to test if two objects are deeply equal:

const assert = require('assert');

// Define two objects with different types of values
let obj3 = {
  name: 'Alice',
  age: '25', // string
  hobbies: ['reading', 'writing', 'coding'],
  address: {
    city: 'New York',
    zip: 10001
  }
};

let obj4 = {
  name: 'Alice',
  age: 25, // number
  hobbies: ['reading', 'writing', 'coding'],
  address: {
    city: 'New York',
    zip: 10001
  }
};

// Use assert.deepStrictEqual to check if the objects are deeply equal
try {
  assert.deepStrictEqual(obj3, obj4);
  console.log('The objects are deeply equal');
} catch (error) {
  console.error(error);
}

// AssertionError [ERR_ASSERTION]: Expected values to be strictly deep-equal

This example will print an error to the console, because the age property of obj3 is not strictly equal to the age property of obj4.