How to implement a DeepEqual function

As a backend developer, it often occurs that I need to compare two objects, but the simple use of the == or === comparisons won’t suffice because of the distinction between how primitive and reference values are stored and compared in JavaScript.

Here’s a simple implementation to achieve just that for object of any complexity and nesting level:

function deepEqual(a, b) {
  if (a === b) return true

  if (a == null || typeof a != 'object' || b == null || typeof b != 'object')
    return false

  let keysA = Object.keys(a),
    keysB = Object.keys(b)

  if (keysA.length != keysB.length) return false

  for (let key of keysA) {
    if (!keysB.includes(key) || !deepEqual(a[key], b[key])) return false
  }

  return true
}

// Usage
let obj1 = { personal: { name: 'Salva' }, serial: 1234567890 }
let obj2 = { personal: { name: 'Salva' }, serial: 1234567890 }

console.log(deepEqual(obj1, obj2)) // true

The deepEqual function is designed to compare two values, a and b, to determine if they are deeply equal. By “deeply equal”, it means that if a and b are objects, not only should they have the same properties with the same names, but the values of these properties should also be equal, even if they are objects themselves.

Let’s break down the function step-by-step:

1. Primitive Value Comparison

if (a === b) return true

If both a and b are strictly equal (i.e., they reference the same value or object in memory), the function immediately returns true.

2. Null or Non-object Check

if (a == null || typeof a != 'object' || b == null || typeof b != 'object')
  return false

This part checks if either a or b is null or not an object. If either of them is, the function immediately returns false since deep equality is only concerned with comparing object structures.

3. Comparing Object Keys

let keysA = Object.keys(a),
  keysB = Object.keys(b)

Here, the function gets all the property names (keys) of both objects a and b.

if (keysA.length != keysB.length) return false

If a and b don’t have the same number of properties, they can’t be deeply equal, so the function immediately returns false.

4. Deep Key and Value Comparison

for (let key of keysA) {
  if (!keysB.includes(key) || !deepEqual(a[key], b[key])) return false
}

The function then loops through each key in a and checks:

  • If b also has that key (!keysB.includes(key)).
  • If the values corresponding to that key in both a and b are deeply equal by recursively calling deepEqual. If either of these checks fails for any key, the function immediately returns false.

5. Final return

return true

If the function hasn’t returned false by this point, it means a and b are deeply equal, so it returns true.

Primitive values vs reference values

Primitive values

In JavaScript, primitive values are data that are stored on the stack. They represent a single value and are immutable (cannot be changed). Examples of primitive data types include:

  • Number
  • String
  • Boolean
  • Undefined
  • Null
  • Symbol (ES6)
  • BigInt (recently added)

When you compare two primitive values using either == or ===, you’re comparing their actual values.

Example 1

let a = 'hello'
let b = 'hello'

console.log(a === b) // true

In the example above, both a and b are strings with the value “hello”, so the comparison returns true.

Reference values

Reference values are objects that are stored in the heap. They don’t store the actual object but rather a pointer or reference to the location in memory where the object resides. Examples include:

  • Objects
  • Arrays
  • Functions

When you compare reference values using either == or ===, you’re comparing their references (memory addresses), not their actual content.

Example 2

let obj1 = { key: 'value' }
let obj2 = { key: 'value' }

console.log(obj1 === obj2) // false

In the example above, even though obj1 and obj2 have the exact same content, the comparison returns false. This is because they are two distinct objects stored in different memory locations. When you’re comparing obj1 and obj2, you’re actually comparing their memory addresses, not their content.

Why Direct Object Comparison Doesn’t Work

Because of the way reference values work in JavaScript, directly comparing two objects (or arrays, or functions) using == or === will only return true if both references point to the exact same memory location (i.e., they reference the same object). This means that two objects with identical structure and content will be considered different if they’re located at different memory addresses.

Example 3

let arr1 = [1, 2, 3]
let arr2 = [1, 2, 3]

console.log(arr1 === arr2) // false

Both arrays have the same content, but since they occupy different memory locations, they’re considered different.

Conclusion

To accurately determine if two objects (or arrays) are structurally and content-wise identical, you’d need to perform a deep comparison, like the deepEqual function discussed earlier. Simple == or === comparisons won’t suffice because of the distinction between how primitive and reference values are stored and compared in JavaScript.