hardJS ES5#141

deepEqual from scratch

Prompt

Implement deepEqual(a, b) — structural equality:

  1. Primitives compare by value; NaN equals NaN (unlike ===)
  2. Arrays: same length, elements deep-equal in order
  3. Plain objects: same key set (an explicit undefined value still counts as a key), values deep-equal
  4. null equals only null — remember typeof null === 'object'
  5. No JSON.stringify — it lies about key order, undefined, and NaN

Solution

function deepEqual(a, b) {
  // Fast path — also handles primitives and reference-equal objects
  if (a === b) return true
  // NaN is the only value !== itself
  if (Number.isNaN(a) && Number.isNaN(b)) return true
  // null check before typeof: typeof null === 'object'
  if (a === null || b === null) return false
  if (typeof a !== 'object' || typeof b !== 'object') return false
  if (Array.isArray(a) !== Array.isArray(b)) return false

  const keysA = Object.keys(a)
  const keysB = Object.keys(b)
  if (keysA.length !== keysB.length) return false
  return keysA.every(key =>
    Object.prototype.hasOwnProperty.call(b, key) && deepEqual(a[key], b[key])
  )
}
Mentor's take

Interviewers ask for deepEqual because the naive answer — JSON.stringify(a) === JSON.stringify(b) — fails in exactly the ways that matter in production: key order changes the string, undefined values vanish, NaN becomes null, and a Date silently becomes a string. Naming those failures before writing code is the senior move.

What the reference solution encodes:

  • a === b first is both the primitive comparison and a reference-equality fast path — comparing an object to itself costs O(1), which matters when this runs in a memoized selector.
  • The null guard sits before typeof because typeof null === 'object' is a 30-year-old bug you're expected to know by name.
  • Key-count comparison + hasOwnProperty catches both extra keys and inherited keys. Using b[key] !== undefined as an existence check is the classic subtle bug — it conflates "missing" with "present but undefined".
  • Scope honestly: this doesn't handle Date, Map/Set, RegExp, or cycles (a cyclic structure recurses forever — production versions carry a WeakMap of visited pairs). Saying "I'd add a WeakMap for cycles" unprompted is a strong signal.

The React connection: this is what React.memo's comparator could do but deliberately doesn't — deep comparison costs O(tree size) per render, which is why React bets on shallow equality plus immutable updates instead.

Say it: "I check reference equality first, handle NaN and null explicitly, then compare key sets recursively — and I'd flag that cycles need a WeakMap and that this cost is exactly why React chose shallow comparison."