Skip to content

Instantly share code, notes, and snippets.

@nicbell
Last active September 23, 2022 16:56
Show Gist options
  • Star 91 You must be signed in to star a gist
  • Fork 23 You must be signed in to fork a gist
  • Save nicbell/6081098 to your computer and use it in GitHub Desktop.
Save nicbell/6081098 to your computer and use it in GitHub Desktop.
JavaScript object deep comparison. Comparing x === y, where x and y are values, return true or false. Comparing x === y, where x and y are objects, returns true if x and y refer to the same object. Otherwise, returns false even if the objects appear identical. Here is a solution to check if two objects are the same.
//Primitive Type Comparison
var a = 1;
var b = 1;
var c = a;
console.log(a == b); //true
console.log(a === b); //true
console.log(a == c); //true
console.log(a === c); //true
//Object comparison
var a = { blah: 1 };
var b = { blah: 1 };
var c = a;
console.log(a == b); //false
console.log(a === b); //false
console.log(a == c); //true
console.log(a === c); //true
//How To Compare Object Values
var a = { blah: 1 };
var b = { blah: 1 };
var c = a;
var d = { blah: 2 };
Object.compare = function (obj1, obj2) {
//Loop through properties in object 1
for (var p in obj1) {
//Check property exists on both objects
if (obj1.hasOwnProperty(p) !== obj2.hasOwnProperty(p)) return false;
switch (typeof (obj1[p])) {
//Deep compare objects
case 'object':
if (!Object.compare(obj1[p], obj2[p])) return false;
break;
//Compare function code
case 'function':
if (typeof (obj2[p]) == 'undefined' || (p != 'compare' && obj1[p].toString() != obj2[p].toString())) return false;
break;
//Compare values
default:
if (obj1[p] != obj2[p]) return false;
}
}
//Check object 2 for any extra properties
for (var p in obj2) {
if (typeof (obj1[p]) == 'undefined') return false;
}
return true;
};
console.log(Object.compare(a, b)); //true
console.log(Object.compare(a, c)); //true
console.log(Object.compare(a, d)); //false
@GoldStrikeArch
Copy link

GoldStrikeArch commented Jun 22, 2021

Best implementation that I have seen so far is in rambda's "equals" function.

The only thing that I think can be improved (although, I am not sure that this is the right direction) is when both arguments have a "function" type, we could convert the source code to strings and compare them...

Here is the code (I simply put everything into 1 file) from their source:

const _isArray = Array.isArray;

function type(input) {
  const typeOf = typeof input;

  if (input === null) {
    return "Null";
  } else if (input === undefined) {
    return "Undefined";
  } else if (typeOf === "boolean") {
    return "Boolean";
  } else if (typeOf === "number") {
    return Number.isNaN(input) ? "NaN" : "Number";
  } else if (typeOf === "string") {
    return "String";
  } else if (_isArray(input)) {
    return "Array";
  } else if (typeOf === "symbol") {
    return "Symbol";
  } else if (input instanceof RegExp) {
    return "RegExp";
  }

  const asStr = input && input.toString ? input.toString() : "";

  if (["true", "false"].includes(asStr)) return "Boolean";
  if (!Number.isNaN(Number(asStr))) return "Number";
  if (asStr.startsWith("async")) return "Async";
  if (asStr === "[object Promise]") return "Promise";
  if (typeOf === "function") return "Function";
  if (input instanceof String) return "String";

  return "Object";
}

function parseError(maybeError) {
  const typeofError = maybeError.__proto__.toString();
  if (!["Error", "TypeError"].includes(typeofError)) return [];

  return [typeofError, maybeError.message];
}

function parseDate(maybeDate) {
  if (!maybeDate.toDateString) return [false];

  return [true, maybeDate.getTime()];
}

function parseRegex(maybeRegex) {
  if (maybeRegex.constructor !== RegExp) return [false];

  return [true, maybeRegex.toString()];
}


// main function is here
function equals(a, b) {
  if (arguments.length === 1) return (_b) => equals(a, _b);

  const aType = type(a);
  if (aType !== type(b)) return false;
  if (aType === "Function") {
    return a.name === undefined ? false : a.name === b.name;
  }

  if (["NaN", "Undefined", "Null"].includes(aType)) return true;

  if (aType === "Number") {
    if (Object.is(-0, a) !== Object.is(-0, b)) return false;

    return a.toString() === b.toString();
  }

  if (["String", "Boolean"].includes(aType)) {
    return a.toString() === b.toString();
  }

  if (aType === "Array") {
    const aClone = Array.from(a);
    const bClone = Array.from(b);

    if (aClone.toString() !== bClone.toString()) {
      return false;
    }

    let loopArrayFlag = true;
    aClone.forEach((aCloneInstance, aCloneIndex) => {
      if (loopArrayFlag) {
        if (
          aCloneInstance !== bClone[aCloneIndex] &&
          !equals(aCloneInstance, bClone[aCloneIndex])
        ) {
          loopArrayFlag = false;
        }
      }
    });

    return loopArrayFlag;
  }

  const aRegex = parseRegex(a);
  const bRegex = parseRegex(b);

  if (aRegex[0]) {
    return bRegex[0] ? aRegex[1] === bRegex[1] : false;
  } else if (bRegex[0]) return false;

  const aDate = parseDate(a);
  const bDate = parseDate(b);

  if (aDate[0]) {
    return bDate[0] ? aDate[1] === bDate[1] : false;
  } else if (bDate[0]) return false;

  const aError = parseError(a);
  const bError = parseError(b);

  if (aError[0]) {
    return bError[0]
      ? aError[0] === bError[0] && aError[1] === bError[1]
      : false;
  }

  if (aType === "Object") {
    const aKeys = Object.keys(a);

    if (aKeys.length !== Object.keys(b).length) {
      return false;
    }

    let loopObjectFlag = true;
    aKeys.forEach((aKeyInstance) => {
      if (loopObjectFlag) {
        const aValue = a[aKeyInstance];
        const bValue = b[aKeyInstance];

        if (aValue !== bValue && !equals(aValue, bValue)) {
          loopObjectFlag = false;
        }
      }
    });

    return loopObjectFlag;
  }

  return false;
}

module.exports = equals;

@DLiblik
Copy link

DLiblik commented Jun 22, 2021

+1 like on the solution of @DLiblik posted above, which also covers dates and functions. Maybe post it in a separate github repository so it can be found more easily?

@edwinro - done - find the gist here: areEquivalent.js

@ashishume
Copy link

Covers most cases (including the order of the key property if present)

function isEqual(obj1, obj2) {
  function getType(obj) {
    return Object.prototype.toString.call(obj).slice(8, -1).toLowerCase();
  }

  let type = getType(obj1);

  // If the two items are not the same type, return false
  if (type !== getType(obj2)) return false;
  if (type === "array") return areArraysEqual();
  if (type === "object") return areObjectsEqual();
  if (type === "function") return areFunctionsEqual();

  function areArraysEqual() {
    // Check length
    if (obj1.length !== obj2.length) return false;
    // Check each item in the array
    for (let i = 0; i < obj1.length; i++) {
      if (!isEqual(obj1[i], obj2[i])) return false;
    }
    // If no errors, return true
    return true;
  }
  function areObjectsEqual() {
    if (Object.keys(obj1).length !== Object.keys(obj2).length) return false;
    // Check each item in the object
    for (let key in obj1) {
      if (Object.prototype.hasOwnProperty.call(obj1, key)) {
        if (!isEqual(obj1[key], obj2[key])) return false;
      }
    }
    // If no errors, return true
    return true;
  }
  function areFunctionsEqual() {
    return obj1.toString() === obj2.toString();
  }
  function arePrimativesEqual() {
    return obj1 === obj2;
  }
  return arePrimativesEqual();
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment