MediumGoogle

Deep Equality

Prompt

Write a JavaScript function deepEqual(obj1, obj2) that checks if two given objects (or arrays) are deeply equal.

Playground

Hint 1

Start with the base cases. If both values are the same reference (===), return true. If either is not an object or is null, compare them with === and return the result.

Hint 2

For the recursive case: get the keys of both objects. If they have different numbers of keys, return false. Then loop through each key and recursively call deepEqual on the values. If any pair isn't deeply equal, return false.

Hint 3

This works for arrays too because Object.keys([1, 2, 3]) returns ['0', '1', '2'] and array values can be accessed with bracket notation just like objects.

Solution

Explanation

If you've solved shallow equality, you know its limitation: it can't compare nested objects. Two objects that look identical get reported as "not equal" because the nested objects are different references in memory.

shallowEqual(
{ name: 'Alice', address: { city: 'NY' } },
{ name: 'Alice', address: { city: 'NY' } }
);
// false! The two "address" objects are different references

Deep equality fixes this. Instead of giving up when it hits a nested object, it goes inside that object and compares its contents too. And if those contents have nested objects of their own, it goes inside those as well. It keeps going until it reaches primitive values (numbers, strings, booleans, null) that can be compared with ===.

deepEqual(
{ name: 'Alice', address: { city: 'NY' } },
{ name: 'Alice', address: { city: 'NY' } }
);
// true! It looked inside "address" and compared the contents

The three cases

The function handles values in three stages:

Are they the same thing? If obj1 === obj2, return true immediately. This catches two references to the same object, two identical numbers, two identical strings, both being null, etc. This is also our recursion's exit point for primitives.

if (obj1 === obj2) return true;

Is either one not an object? If we got past the === check and either value is null or isn't an object (it's a number, string, or boolean), then they can't be equal. We already know === failed, so they're different.

if (
obj1 === null ||
obj2 === null ||
typeof obj1 !== 'object' ||
typeof obj2 !== 'object'
) {
return false;
}

Why check for null separately? Because typeof null is 'object' in JavaScript (a famous language bug from 1995). Without the null check, we'd try to call Object.keys(null) and crash.

Both are objects? Compare their contents. Get the keys of both. If the counts don't match, they can't be equal. Then loop through each key and recursively call deepEqual on the values:

const keys1 = Object.keys(obj1);
const keys2 = Object.keys(obj2);

if (keys1.length !== keys2.length) return false;

for (const key of keys1) {
if (
!keys2.includes(key) ||
!deepEqual(obj1[key], obj2[key])
) {
return false;
}
}

return true;

How the recursion works

Let's trace through deepEqual({ a: 1, b: { c: 2 } }, { a: 1, b: { c: 2 } }):

  1. Both are objects with keys ['a', 'b']. Same count. Start comparing.
  2. Key 'a': deepEqual(1, 1). They're the same primitive (1 === 1), returns true.
  3. Key 'b': deepEqual({ c: 2 }, { c: 2 }). Both are objects. Recurse.
    • Inside: keys are ['c']. Same count.
    • Key 'c': deepEqual(2, 2). Same primitive, returns true.
    • All keys matched, return true.
  4. All keys matched at the top level too. Return true.

The recursion peels off one layer at a time. It keeps going deeper until it hits primitives, which get compared with ===. Then it unwinds back up, and if everything matched at every level, the final answer is true.

Why this works for arrays too

Arrays in JavaScript are just objects with numeric keys. Object.keys([10, 20, 30]) returns ['0', '1', '2']. So our function compares arrays element by element without needing any special array handling:

deepEqual([1, [2, 3]], [1, [2, 3]]);
// Object.keys gives ['0', '1'] for both
// deepEqual(1, 1) → true
// deepEqual([2, 3], [2, 3]) → recurse → true

It even handles mixed structures like { a: [1, { b: 2 }] } because the recursion doesn't care whether it's looking at an object or an array. It just compares keys and values all the way down.

Shallow vs Deep: when to use which

Shallow equality is faster and works great for flat objects. React uses it internally for React.memo and useMemo dependency checks. If your data is flat or you intentionally use the same references for nested objects, shallow equality is all you need.

Deep equality is for when you need to know if two complex data structures have the exact same content. Think of comparing API responses, configuration objects, or writing test assertions. It's more thorough but also more expensive since it visits every property recursively.

Performance

Deep equality visits every property in both objects recursively. For very large or deeply nested objects, this can be slow. In performance-critical code, shallow equality with well-structured data is usually a better choice. Libraries like Lodash provide an optimized _.isEqual for production use.