DEV Community

Cover image for How to Get a Perfect Deep Equal in JavaScript
Zachary Lee
Zachary Lee

Posted on • Originally published at webdeveloper.beehiiv.com on

How to Get a Perfect Deep Equal in JavaScript

In JavaScript, we can use == , === operator and Object.is method to judge the equality of two variable values. But if we want to compare two variable values deeply, can they meet our needs?


==

The == operator is a loose equality operator. When comparing two different types of values, it will first try to convert them to the same type, and then compare them. The specific algorithm used is Abstract Equality Comparison Algorithm, its rules are complex and difficult to remember, you can also check its short description here, and you can also experience its conversion process interactively through this link.

It is arguably the least rigorous equality operator, and many results may surprise you.

===

The === operator is a strict equality operator. It always considers operands of different types to be different. The specific algorithm used is Strict Equality Comparison Algorithm, its rules are easier to remember, you can also check its short description here.

It is our most used equality operator. It looks strict, but it has flaws in the following cases:

  • Numbers must have the same numeric values. +0 and -0 are considered to be the same value.

  • If either operand is NaN, it will return false.

  • If both operands are objects, it will only judge whether their reference addresses are the same.

Object.is

The Object.is method behaves the same as the === operator in most cases, but has the opposite result of the === operator in the following two cases:

  • +0 and -0 are considered different values and will return false.

  • NaN and NaN are considered to be the same value and will return true.

It doesn’t mean that Object.isis stricter than === operator. We should deal with the corresponding usage requirements according to the specific characteristics of Object.is .

Note that Object.is behaves the same as the === operator if the two operands are objects, or only determines whether their reference addresses are the same.


How to get a Deep Equal

For the case where the operands are all objects, we expect Deep Equal to give the answer we want. For example, for any non-primitive objects x and y which have the same structure but are distinct objects themselves, we would expect Deep Equal to return true.

I explained the characteristics of JavaScript data types in the How to Get a Perfect Deep Copy in JavaScript article published earlier. It is precise because of these characteristics that a perfect Deep Equal needs to consider many edge cases, and its performance is destined to be poor. , so in React, we do not use Deep Equal to judge whether the state has changed before and after, but Shallow Equal.

So let’s take a look at how Shallow Equal in React is implemented?

Shallow Equal in React

I don’t change the original logic here, just remove the compatibility code to improve readability. Its original file is here, you can check it out for comparison.

function shallowEqual(objA, objB) {
  // P1
  if (Object.is(objA, objB)) {
    return true;
  }

  // P2
  if (
    typeof objA !== 'object' ||
    objA === null ||
    typeof objB !== 'object' ||
    objB === null
  ) {
    return false;
  }

  // P3
  var keysA = Object.keys(objA);
  var keysB = Object.keys(objB);

  if (keysA.length !== keysB.length) {
    return false;
  }

  for (var i = 0; i < keysA.length; i++) {
    if (
      !Object.prototype.hasOwnProperty.call(objB, keysA[i]) ||
      !Object.is(objA[keysA[i]], objB[keysA[i]])
    ) {
      return false;
    }
  }

  return true;
}
Enter fullscreen mode Exit fullscreen mode

Note my comments in the code that start with P , I’ll explain in units of it:

P1: The first-level comparison is performed through Object.is, and if it is equal, it returns true, which has the effect of first-level filtering.

P2: Make sure both are objects and return false if either is not.

P3: At this point both are objects, but their reference addresses are not the same. So the next step is to loop through the key array of one of the objects, determine whether the key is its own property (as opposed to inheriting it) of another object, and determine whether the value corresponding to the key of these two objects can pass Object.is , that is, here React does not choose recursive judgment for performance, which means that only one layer is compared.

After reading the Shallow Equal in React, I believe you have some ideas. Let’s realize a perfect Deep Equal based on it.

Get the perfect Deep Equal based on Shallow Equal

The test code here uses the deep copy function implemented in the previous article. Of course, you can also modify and test the results directly on StackBlitz, for example, you can remove the comments at the end of the code to see the change in the result.

const deepEqual = (objA, objB, map = new WeakMap()) => {
  // P1
  if (Object.is(objA, objB)) return true;

  // P2
  if (objA instanceof Date && objB instanceof Date) {
    return objA.getTime() === objB.getTime();
  }
  if (objA instanceof RegExp && objB instanceof RegExp) {
    return objA.toString() === objB.toString();
  }

  // P3
  if (
    typeof objA !== 'object' ||
    objA === null ||
    typeof objB !== 'object' ||
    objB === null
  ) {
    return false;
  }

  // P4
  if (map.get(objA) === objB) return true;
  map.set(objA, objB);

  // P5
  const keysA = Reflect.ownKeys(objA);
  const keysB = Reflect.ownKeys(objB);

  if (keysA.length !== keysB.length) {
    return false;
  }

  for (let i = 0; i < keysA.length; i++) {
    if (
      !Reflect.has(objB, keysA[i]) ||
      !deepEqual(objA[keysA[i]], objB[keysA[i]], map)
    ) {
      return false;
    }
  }

  return true;
};
Enter fullscreen mode Exit fullscreen mode

P1: Like Shadow Equal, use Object.is for first-level filtering.

P2: Special handling is required for Date and RegExp, so use Date.prototype.getTime() for Date to get the timestamp and compare, and RegExp.prototype.toString() for RegExp to get string and compare.

P3: Like Shadow Equal, make sure both are objects and return false if either is not.

P4: Use WeakMap as a hash table to solve the circular reference problem. If the two have been compared before, it will return true, which means that it will not affect the final result.

P5: Compared to Shadow Equal, we upgrade to Reflect.ownKeys to get all keys. Then we also judge the length of the attribute array, and then loop through all the attribute keys of objA, and use Reflect.has to judge whether there are the same attributes on objB. Finally, we upgrade Object.is to recursive processing, and constantly judge whether the deep values are equal.

If you look at the deep copy functions implemented earlier, do you feel similarities between them? Yes, they are very similar in some logical judgments. You can learn by comparison. I believe it can deepen your understanding of JavaScript data structure.


If you find this helpful, please consider subscribing to my newsletter for more insights on web development. Thank you for reading!

Top comments (0)