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.