Shallow Comparison vs Deep Comparison in Javascript
Shallow Comparison vs Deep Comparison in Javascript

You most likely heard terms shallow comparison and deep comparison. In this post you will fully understand how it all works.

In this tutorial you will learn:
- Problems of standard Javascript comparison
- Implement shallow comparison
- Implement deep comparison
- Compare all of them

So just to be on the same page in Javascript we have default comparison of the variables which can't solve some problems, shallow comparison and deep comparison.

Standard Javascript comparison

Why it's not enough to use standard comparison of Javascript? We use === to compare 2 properties

const a = 1
const b = 2

console.log(a === b)

And it will work fine for primitives like numbers, booleans, strings. But it won't work like you might expect for objects and arrays

const a = [1]
const b = [2]

console.log(a === b)

As you can see we got false because when we compare primitives Javascript compares values and when we compare arrays or objects Javascript compares if they are referenced the same object.

So actually what most people want when they write their code is to compare values inside arrays or objects.

This is the mistake that all beginners do. They want to copy object and change it's value but it doesn't work as intended.

const a = {name: 'foo'}
const b = a
b.age = 30
console.log(a, b)

As you can see this code modifies body a and b because they reference the same object in memory.

Shallow comparison

Which brings us to the shallow comparison. It's a custom function which can compare different data types. Most important point to remember that shallow equal is much faster than deep equal.

const typeOf = (input) => {
  const rawObject = Object.prototype.toString.call(input).toLowerCase();
  const typeOfRegex = /\[object (.*)]/g;
  const type = typeOfRegex.exec(rawObject)[1];
  return type;
};

So here I have a helper typeOf function which simply returns the type of data as a string. As you can see it returns correct types.

typeOf('1')
typeOf(1)
typeOf([1])
typeOf({a: 1})

Now let's create our shallow equality function.

First of all we want to check if 2 things that we compare are of different type. If it's different than they can't be equal.

const shallowCompare = (source, target) => {
  if (typeOf(source) !== typeOf(target)) {
    return false;
  }
};

Now we can simply add default Javascript comparison at the end of our function.

const shallowCompare = (source, target) => {
  if (typeOf(source) !== typeOf(target)) {
    return false;
  }

  return source === target;
};

So it already works for primitives. Now we need to add logic how we will check arrays. And we don't want to do any deep checking. We simply check if every element equals the same element in other array by using every function. But again it's plain Javascript we don't do any deep equal and this code won't work correctly with objects or arrays inside each array. But for primitives inside it will work correctly which is completely fine for us as we go for performance here.

const shallowCompare = (source, target) => {
  if (typeOf(source) !== typeOf(target)) {
    return false;
  }

  if (typeOf(source) === "array") {
    if (source.length !== target.length) {
      return false;
    }
    return source.every((el, index) => el === target[index]);
  }

  return source === target;
};

The next step is to compare objects. Again no deep comparison here, we simply go through keys and compare their values. If our object nested it won't be checked.

const shallowCompare = (source, target) => {
  if (typeOf(source) !== typeOf(target)) {
    return false;
  }

  if (typeOf(source) === "array") {
    if (source.length !== target.length) {
      return false;
    }
    return source.every((el, index) => el === target[index]);
  } else if (typeOf(source) === "object") {
    return Object.keys(source).every((key) => source[key] === target[key]);
  } else if (typeOf(source) === "date") {
    return source.getTime() === target.getTime();
  }

  return source === target;
};

And the last thing is to check dates.

const shallowCompare = (source, target) => {
  if (typeOf(source) !== typeOf(target)) {
    return false;
  }

  if (typeOf(source) === "array") {
    if (source.length !== target.length) {
      return false;
    }
    return source.every((el, index) => el === target[index]);
  } else if (typeOf(source) === "object") {
    return Object.keys(source).every((key) => source[key] === target[key]);
  } else if (typeOf(source) === "date") {
    return source.getTime() === target.getTime();
  }

  return source === target;
};

We convert dates to milliseconds and compare them.

And here is the usage of shallowCompare.

shallowCompare({a: 1}, {a: 1}) // true

So if now works how it should work by default in Javascript. But obviously deep comparison will fail here.

shallowCompare({a: {b: 1}}, {a: {b: 1}}) // false

Deep comparison

But sometimes we really want to compare big difficult nested objects or arrays. And it can't be fast. We should do it recursive and it will be slow. This is why I recommend you to avoid comparing all properties of huge objects when possible. I will copy parse our shallowCompare function completely as it is 99% the same code.

const deepCompare = (source, target) => {
  if (typeOf(source) !== typeOf(target)) {
    return false;
  }

  if (typeOf(source) === "array") {
    if (source.length !== target.length) {
      return false;
    }

    return source.every((entry, index) => deepCompare(entry, target[index]));
  } else if (typeOf(source) === "object") {
    if (Object.keys(source).length !== Object.keys(target).length) {
      return false;
    }

    return Object.keys(source).every((key) =>
      deepCompare(source[key], target[key])
    );
  } else if (typeOf(source) === "date") {
    return source.getTime() === target.getTime();
  }

  return source === target;
};

In deep comparison we do similar stuff but instead of using plain Javascript comparison we call our deepCompare function recursively. It means for example that if inside object we have a property which is an object we will start this function again and check the types of data inside and if it's an object again we will start deepCompare recursively again.

Exactly the same we do with array. We check recursively every single element.

Here is how our deepCompare is working.

deepCompare({a: {b: 1}}, {a: {b: 1}}) // true

As we compare every single value it is the most correct way of doing things.

Conclusion

So here is a conclusion. For primitives you can simply use plain Javascript. If you need fast comparison of simple arrays and objects use shallow comparison. If it's important for you to check every single key inside and compare them then use deep comparison but you need to be aware that it is the slowest way to compare things.

And you should not build it on your own in popular libraries like Lodash or Ramda or just as npm packages you can find both this implementations.

And if you are interested is learning pure functions and side effects in Javascript make sure to check this post.

📚 Source code of what we've done