Vanilla JS Deep Object Comparison

October 17, 2019

I very randomly got curious how to compare objects in Javascript so opened DevTools and began tinkering. I remembered you could use JSON.stringify() to do this, and it even works with deep equality. Example: JSON.stringify({a: 1, b: {c: 2}}) === JSON.stringify({a: 1, b: {c: 2}})

But it breaks the second those properties aren’t in the same order. For instance, JSON.stringify({a: 1, b: 2}) === JSON.stringify({b: 2, a: 1}) fails.

Like most deep things, I figured a recursive function would be necessary, and out of laziness, started Googling around to see if there was a really slick ES6 way to do it (like some fresh/obscure Object.deepEqual native method I hadn’t learned yet or something), but alas, there wasn’t.

I was certain Lodash had something. And of course it did. And in practice, I would probably use it because it’s probably better tested for fringe cases, probably pretty performant, and Lodash is familiar to all developers and comes with lots of usage documentation.

But I was curious how it worked so I dove into Lodash code for a second and it was depending on sooo many little stupid building block modules for every single thing, that reverse engineering it made the problem “not fun”.

So, I just figured I’d try to solve it myself (which I’m proud to say I did really quickly—usually these things hamstring me for a couple horus but I solved this one instantly) and submit it to StackOverflow (since its an incredibly popular question, and my solution is elegant, maybe I could get some of that sweet SO karma), but all the threads for it were locked, so HERE WE ARE. 😩

const nestedObj1 = { b: 2, c: { e: 3, d: 4 }, a: 1 }
const nestedObj2 = { a: 1, b: 2, c: { d: 4, e: 3 } }
const nestedObj3 = { a: 1, b: 2, c: { d: 4, e: 9999999999999 } }
const shallowObj1 = { a: 1, b: 2 }
const shallowObj2 = { b: 2, a: 1 }
const shallowObj3 = { a: 1, b: 9999999999999 }

// Recursively sorts the keys and returns a fresh object
function sortObj(obj) {
  return Object
    .keys(obj)
    .sort()
    .reduce((acc, curr) => {
      if (typeof obj[curr] === 'object') {
        acc[curr] = sortObj(obj[curr])
      } else {
        acc[curr] = obj[curr]
      }

      return acc
    }, {})
}

// Does that stringify comparison stuff to the _now 100% recursively sorted_ objects
function deepEqual(obj1, obj2) {
  return JSON.stringify(sortObj(obj1)) === JSON.stringify(sortObj(obj2))
}

deepEqual(nestedObj1, nestedObj2) // true
deepEqual(nestedObj1, nestedObj3) // false
deepEqual(shallowObj1, shallowObj2) // true
deepEqual(shallowObj1, shallowObj3) // false

That’s it. Curious how it performs vs Lodash (maybe my next post should be figuring out how to do isolated function benchmarks 🤔), but it seems to work well in these few generic tests.

Update: As it turns out my implementation is about 35% slower than Lodash’s. 🤷‍♂️ Ah well, it was fun to tinker with.