Javascript Interview Question: Write a function that deep compares two objects
Explore the Inner Workings of deepCompare
Function for Accurate Object Equality Checks
Why we need deep compare function?
The ===
equality operator in JavaScript checks for reference equality, not structural equality. If you have two objects with the same structure and values but are different instances, ===
would consider them unequal. A deep comparison function allows you to check for structural equality.
function deepCompare(obj1, obj2) {
// Check if both parameters are objects
if (typeof obj1 !== 'object' || obj1 === null || typeof obj2 !== 'object' || obj2 === null) {
return obj1 === obj2;
}
// Get the keys of each object
const keys1 = Object.keys(obj1);
const keys2 = Object.keys(obj2);
// Check if the number of keys is the same
if (keys1.length !== keys2.length) {
return false;
}
// Iterate through keys and recursively compare values
for (const key of keys1) {
if (!keys2.includes(key) || !deepCompare(obj1[key], obj2[key])) {
return false;
}
}
// If all checks passed, the objects are deeply equal
return true;
}
Now Let’s break down each line:
1 Function Declaration:
function deepCompare(obj1, obj2) {
This declares a function named deepCompare
that takes two parameters, obj1
and obj2
, representing the objects to be compared.
2 Type and Null Check:
if (typeof obj1 !== 'object' || obj1 === null || typeof obj2 !== 'object' || obj2 === null) {
return obj1 === obj2;
}
This part of the function checks the types and null values of obj1
and obj2
. The purpose of this check is to handle the base case of the recursion and also to handle the scenario where one or both of the objects are not actually objects.
typeof obj1 !== 'object' || obj1 === null
: This checks ifobj1
is not of type 'object' or if it isnull
. If either condition is true, it meansobj1
is not an object, and the function comparesobj1
andobj2
directly (return obj1 === obj2
).typeof obj2 !== 'object' || obj2 === null
: Similarly, this checks ifobj2
is not of type 'object' or if it isnull
. If either condition is true, it meansobj2
is not an object, and the function comparesobj1
andobj2
directly (return obj1 === obj2
).
The purpose of these checks is to handle scenarios where the function is called with non-object values or null
. In such cases, there is no need to perform a deep comparison, and the function can directly compare the values.
3 Get Keys of Objects:
const keys1 = Object.keys(obj1);
const keys2 = Object.keys(obj2);
These lines retrieve the keys of each object using Object.keys()
.
4 Check Number of Keys:
if (keys1.length !== keys2.length) {
return false;
}
This checks if the number of keys in both objects is the same. If not, the objects cannot be deeply equal, and the function returns false.
5 Recursive Comparison:
for (const key of keys1) {
if (!keys2.includes(key) || !deepCompare(obj1[key], obj2[key])) {
return false;
}
}
This loop iterates through the keys of the first object (keys1
). For each key, it checks if the second object (obj2
) contains the same key and recursively compares the values using deepCompare
. If any comparison returns false
, the function immediately returns false
.
6 Final Return:
return true;
If all checks pass, the function returns true
, indicating that the objects are deeply equal.
This line is the final return statement of the function. If the function has passed all the previous checks and iterations without returning false
, it means that the two objects are deeply equal. Therefore, the function returns true
.
- If the function reaches this point, it indicates that all the key-value pairs in the objects (and their nested objects) have been successfully compared, and none of the comparisons resulted in a mismatch.
- Returning
true
signals that the two objects are deeply equal, satisfying the conditions for a deep comparison.
Usage
const objA = { a: 1, b: { c: 2 } };
const objB = { a: 1, b: { c: 2 } };
const objC = { a: 1, b: { c: 3 } };
console.log(deepCompare(objA, objB)); // Output: true
console.log(deepCompare(objA, objC)); // Output: false
Downside of above function:
1. Circular References:
If the objects being compared contain circular references (i.e., an object references itself), the function may enter into an infinite loop and cause a stack overflow. Handling circular references requires additional logic to keep track of visited objects.
2. Prototype Chain:
The function does not consider the prototype chain. If objects have properties defined in their prototypes, those properties won't be included in the comparison. To include prototype properties, additional logic is needed to traverse the prototype chain.
3. Non-Enumerable Properties:
Properties that are not enumerable (e.g., those created with Object.defineProperty
with enumerable: false
) will not be included in the comparison. This might lead to unexpected results if you rely on non-enumerable properties.
4. Sparse Arrays:
Sparse arrays (arrays with holes, i.e., undefined values at certain indices) might not be handled correctly. The function treats an array with a hole as if it's a shorter array, potentially leading to incorrect comparisons.
5. Date Objects:
Date objects are not handled specifically in this function. If you need to compare Date objects, you might want to add special handling for them.
6. Function Properties:
If the objects being compared contain function properties, the function does not compare the function bodies. It only checks if the function references are the same.
7 .Performance for Large Objects:
The function may not be optimal for very large objects due to its recursive nature. For extremely deep or complex objects, it might result in a large number of recursive calls.
Conclusion: This answer might help you to clear your interview. But in production consider using Loadash or any other good third party library.