JavaScript Object Equality Checker
Comparing JavaScript objects for equality can be trickier than it seems. Unlike primitive types (like numbers or strings) where === does a direct value comparison, === on objects checks for reference equality – meaning they are the exact same object in memory. This challenge requires you to implement a function that performs a deep comparison of two JavaScript objects, checking if they have the same keys and values, recursively. This is a fundamental skill for testing, state management, and data manipulation in JavaScript.
Problem Description
Your task is to create a JavaScript function named deepEqual that accepts two arguments, obj1 and obj2, and returns true if they are deeply equal, and false otherwise.
Key Requirements:
- The function must recursively compare nested objects and arrays.
- It should handle primitive types correctly (numbers, strings, booleans, null, undefined, symbols, bigints).
- It should consider the order of keys in objects when comparing (i.e.,
{ a: 1, b: 2 }is not equal to{ b: 2, a: 1 }unless you decide to ignore key order, which for this challenge, we will not). - It should consider the order of elements in arrays.
- The function should handle circular references gracefully, preventing infinite recursion.
Expected Behavior:
- If
obj1andobj2are strictly equal (===), returntrue. - If
obj1orobj2are not objects or are null, compare them using strict equality. - If they are arrays, compare their lengths and then recursively compare each element.
- If they are plain objects, compare the number of keys. Then, compare each key-value pair from
obj1with the corresponding key-value pair inobj2.
Edge Cases to Consider:
- Comparing an object with an array.
- Comparing objects with
nullorundefined. - Comparing objects with different types of values (e.g., number vs. string).
- Objects with no keys.
- Arrays with no elements.
- Circular references within objects or arrays.
Examples
Example 1:
Input: obj1 = { a: 1, b: { c: 3 } }, obj2 = { a: 1, b: { c: 3 } }
Output: true
Explanation: Both objects have the same keys and nested values in the same order.
Example 2:
Input: obj1 = { a: 1, b: 2 }, obj2 = { b: 2, a: 1 }
Output: false
Explanation: The order of keys differs, and for this challenge, key order matters.
Example 3:
Input: obj1 = [1, [2, 3]], obj2 = [1, [2, 3]]
Output: true
Explanation: Both arrays have the same elements in the same order, including nested arrays.
Example 4:
Input: obj1 = { a: 1 }, obj2 = { a: '1' }
Output: false
Explanation: The value for key 'a' has different types (number vs. string).
Example 5:
Input: obj1 = 5, obj2 = 5
Output: true
Explanation: Primitive values are compared using strict equality.
Example 6:
Input: obj1 = { a: 1 }, obj2 = null
Output: false
Explanation: Comparing an object with null.
Example 7:
Input: obj1 = {}, obj2 = {}
Output: true
Explanation: Two empty objects are considered equal.
Example 8: (Circular Reference)
Input:
let circularObj1 = { name: 'obj1' };
circularObj1.self = circularObj1;
let circularObj2 = { name: 'obj1' };
circularObj2.self = circularObj2;
Output: true
Explanation: The circular references are handled correctly, recognizing that both objects point back to themselves in the same manner.
Example 9: (Different Circular Reference)
Input:
let circularObj1 = { name: 'obj1' };
circularObj1.self = circularObj1;
let circularObj2 = { name: 'obj2' };
circularObj2.self = circularObj2;
Output: false
Explanation: Although circular, the top-level objects have different properties.
Constraints
- The function should be able to handle objects and arrays containing primitive types, nested objects, and nested arrays.
- The comparison should be case-sensitive for string values.
- Performance is important; the solution should avoid unnecessary iterations. For very large, deeply nested objects, extremely inefficient solutions might time out.
- The function should return
falseimmediately if a mismatch is found at any level of recursion.
Notes
- Consider how you will differentiate between plain objects and other types of objects (like Dates, Regex, etc.). For this challenge, focus on "plain" JavaScript objects (
{}) and arrays ([]). - A common approach for handling circular references involves using a
SetorMapto keep track of objects that have already been compared during the current comparison path. - Think about the order of checks. For example, checking for strict equality first can be an optimization.
- When comparing object keys, ensure you are comparing the exact keys present in both objects.