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 and obj2 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 that obj1 and obj2 are deeply equal because their structures and values match at all levels.

Handling Edge Cases

  1. Circular References: Objects that reference themselves.
  2. Date Objects: Comparing Date objects.
  3. 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 a Map to track seen objects.