Implementing Custom `deepEqual` in JavaScript
Implementing a custom deepEqual
function in JavaScript is a common interview question. This function compares two values to determine if they are deeply equal, meaning all nested properties and values are also equal.
What is Deep Equality?
Deep equality means that two values are considered equal if they have the same structure and corresponding values at all levels of nesting. This is different from shallow equality, which only checks if the top-level properties are equal.
Real Interview Insights
Interviewers might ask you to:
- Implement a
deepEqual
function. - Explain the difference between shallow and deep equality.
- Handle edge cases, such as circular references and different data types.
Implementing Custom deepEqual
Here's a basic implementation of a deepEqual
function:
function deepEqual(obj1, obj2) {
if (obj1 === obj2) {
return true;
}
if (obj1 == null || obj2 == null || typeof obj1 !== 'object' || typeof obj2 !== 'object') {
return false;
}
const keys1 = Object.keys(obj1);
const keys2 = Object.keys(obj2);
if (keys1.length !== keys2.length) {
return false;
}
for (let key of keys1) {
if (!keys2.includes(key) || !deepEqual(obj1[key], obj2[key])) {
return false;
}
}
return true;
}
Explanation:
- Primitive Value Check: If
obj1
andobj2
are strictly equal, they are deeply equal. - Null and Object Type Check: If either value is
null
or not an object, they are not deeply equal. - Keys Length Check: If the objects have a different number of keys, they are not deeply equal.
- Recursive Check: Recursively check each property for deep equality.
Practical Example
Consider an example with nested objects:
const obj1 = {
a: 1,
b: {
c: 2,
d: [3, 4]
}
};
const obj2 = {
a: 1,
b: {
c: 2,
d: [3, 4]
}
};
console.log(deepEqual(obj1, obj2)); // Output: true
In this example:
- The
deepEqual
function correctly identifies thatobj1
andobj2
are deeply equal because their structures and values match at all levels.
Handling Edge Cases
- Circular References: Objects that reference themselves.
- Date Objects: Comparing Date objects.
- Functions: Ignoring functions or considering them equal only if they reference the same function.
Enhanced Implementation with Circular Reference Handling
function deepEqual(obj1, obj2, seen = new Map()) {
if (obj1 === obj2) {
return true;
}
if (obj1 == null || obj2 == null || typeof obj1 !== 'object' || typeof obj2 !== 'object') {
return false;
}
if (seen.has(obj1) && seen.get(obj1) === obj2) {
return true;
}
seen.set(obj1, obj2);
const keys1 = Object.keys(obj1);
const keys2 = Object.keys(obj2);
if (keys1.length !== keys2.length) {
return false;
}
for (let key of keys1) {
if (!keys2.includes(key) || !deepEqual(obj1[key], obj2[key], seen)) {
return false;
}
}
return true;
}
Practical Example with Circular References
const obj1 = { a: 1 };
obj1.self = obj1;
const obj2 = { a: 1 };
obj2.self = obj2;
console.log(deepEqual(obj1, obj2)); // Output: true
In this example:
- The
deepEqual
function correctly handles circular references by using aMap
to track seen objects.