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
@edwinro
Copy link

edwinro commented Jul 9, 2020

I understood JSON.stringify() is not very performant, and misses if child objects are in a different order.

I've updated the initial code to support arrays (order should be respected), and also 'null vs {}' checks.

No warranties on the code ;)

function compare (obj1, obj2) {

        //check type at start
        if (typeof (obj1) !== typeof (obj2)) return false;
        if (Array.isArray(obj1) !== Array.isArray(obj2)) return false;

        //case no children
        if (obj1 && Object.keys(obj1).length === 0) return (obj1 === obj2);
        if (obj1 == null) return (obj1 === obj2); //case if obj1 is nullish

        //in case of an array
        if (Array.isArray(obj1)) {
            for (let i = 0; i < obj1.length; i++) {
                if (!compare(obj1[i], obj2[i])) return false;
            }

            return true;
        } else {

            //general object case
            //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 (!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;
        }
};

@meetdheeraj
Copy link

function ObjectCompare(obj1, obj2) {
     return !( obj1 < obj2 || obj1 > obj2);
}

Worked for me

@clibu
Copy link

clibu commented Aug 17, 2020

@edwinro Thx for you compare() code. I have found a couple of issues:

// case no children
if (obj1 && Object.keys(obj1).length === 0) return (obj1 === obj2);

does not work correctly. ex.

const obj1 = {}, obj2 = {}
console.log( obj1 === obj2 )  - false

Changed to: return Object.keys( obj2 ).length === 0 fixes this.

Next:

// case if obj1 is nullish
if (obj1 == null) return (obj1 === obj2); 

fails for const obj1 = null, obj2 = undefined

Changed to: return (obj1 == obj2); fixes this. ie. Double equals vs triple equals.

Next:

            //Check object 2 for any extra properties
            for (var p in obj2) {
                if (typeof (obj1[p]) == 'undefined') return false;
            }

does not work correctly if obj2[p] is also undefined:
Fix is to change: if (typeof (obj1[p]) == 'undefined') return false; to if ( obj1[p] === undefined && obj1[2] !== undefined ) return false;

@edwinro
Copy link

edwinro commented Aug 17, 2020

@clibu thank you for your corrections.

Updated thanks to your remarks

    function compare (obj1, obj2) {

        //check type at start
        if (typeof (obj1) !== typeof (obj2)) return false;
        if (Array.isArray(obj1) !== Array.isArray(obj2)) return false;

        //case no children
        if (obj1 && Object.keys(obj1).length === 0) return (Object.keys( obj2 ).length === 0);
        if (obj1 == null) return (obj1 === obj2); //case if obj1 is nullish

        //in case of an array
        if (Array.isArray(obj1)) {
            for (let i = 0; i < obj1.length; i++) {
                if (!compare(obj1[i], obj2[i])) return false;
            }

            return true;
        } else {

            //general object case
            //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 (!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 ( obj1[p] === undefined && obj2[p] !== undefined ) return false;
            }

            return true;
        }
};

@clibu
Copy link

clibu commented Aug 18, 2020

@edwinro Thanks for the update. I snuck in an edit re. an issue with the nullish test which you understandably missed. ie. == vs ===

@edwinro
Copy link

edwinro commented Aug 18, 2020

@edwinro Thanks for the update. I snuck in an edit re. an issue with the nullish test which you understandably missed. ie. == vs ===

@clibu thanks. The if (typeof (obj1) !== typeof (obj2)) return false; catches already the case where obj 1= null and obj2 = undefined. The if statement then returns false.

typeof (null) !== typeof (undefined)
true

The question is , do you want to let the compare function return false if one object is undefined and the other is null? I take the assumption one does indeed wants to detect this difference. ( undefined means a variable has been declared but has not yet been assigned a value, whereas null is an assignment value).

If not, your code suggestion can be applied, but should be put as a first line. Also, the last check if ( obj1[p] === undefined && obj2[p] !== undefined ) return false; would then be replace by if ( obj1[p] === undefined && obj2[p] != undefined ) return false; to also account for null children vs undefined children.

@clibu
Copy link

clibu commented Aug 18, 2020

@edwinro I had picked up on the if (typeof (obj1) !== typeof (obj2)) return false; handling this and I agree that is what you'd want.

The nullish comment threw me as nullish means null or undefined. I'm not sure why this test is there?

FYI I'm in Australia, a bit of a time difference. 😀

@xettri
Copy link

xettri commented Oct 29, 2020

will fail for dates as the value of any object

let a = { x : new Date() };
let b = { x : new Date('some-different-date') };

console.log(Object.compare(a, b));  // true --> but actually it should false

@DLiblik
Copy link

DLiblik commented Mar 5, 2021

Here's what I've used over the years - easy to modify for special cases if you need to (i.e. if you want to insert a different compare for functions, etc.):

/**
 Compares two items (values or references) for nested equivalency, meaning that
 at root and at each key or index they are equivalent as follows:

 - If a value type, values are either hard equal (===) or are both NaN
     (different than JS where NaN !== NaN)
 - If functions, they are the same function instance or have the same value
     when converted to string via `toString()`
 - If Date objects, both have the same getTime() or are both NaN (invalid)
 - If arrays, both are same length, and all contained values areEquivalent
     recursively - only contents by numeric key are checked
 - If other object types, enumerable keys are the same (the keys themselves)
     and values at every key areEquivalent recursively

 Author: Dathan Liblik

 License: Free to use anywhere by anyone, as-is, no guarantees of any kind.

 @param value1 First item to compare
 @param value2 Other item to compare
 @param stack Used internally to track circular refs - don't set it
 */
export function areEquivalent(value1, value2, stack=[]) {
  // Numbers, strings, null, undefined, symbols, functions, booleans.
  // Also: objects (incl. arrays) that are actually the same instance
  if (value1 === value2) {
    // Fast and done
    return true;
  }

  const type1 = typeof value1;

  // Ensure types match
  if (type1 !== typeof value2) {
    return false;
  }

  // Special case for number: check for NaN on both sides
  // (only way they can still be equivalent but not equal)
  if (type1 === 'number') {
    // Failed initial equals test, but could still both be NaN
    return (isNaN(value1) && isNaN(value2));
  }

  // Special case for function: check for toString() equivalence
  if (type1 === 'function') {
    // Failed initial equals test, but could still have equivalent
    // implementations - note, will match on functions that have same name
    // and are native code: `function abc() { [native code] }`
    return value1.toString() === value2.toString();
  }

  // For these types, cannot still be equal at this point, so fast-fail
  if (type1 === 'bigint' || type1 === 'boolean' ||
    type1 === 'function' || type1 === 'string' ||
    type1 === 'symbol')
  {
    return false;
  }

  // For dates, cast to number and ensure equal or both NaN (note, if same
  // exact instance then we're not here - that was checked above)
  if (value1 instanceof Date) {
    if (!(value2 instanceof Date)) {
      return false;
    }
    // Convert to number to compare
    const asNum1 = +value1, asNum2 = +value2;
    // Check if both invalid (NaN) or are same value
    return asNum1 === asNum2 || (isNaN(asNum1) && isNaN(asNum2));
  }

  // At this point, it's a reference type and could be circular, so
  // make sure we haven't been here before... note we only need to track value1
  // since value1 being un-circular means value2 will either be equal (and not
  // circular too) or unequal whether circular or not.
  if (stack.includes(value1)) {
    throw new Error(`areEquivalent value1 is circular`);
  }

  // breadcrumb
  stack.push(value1);

  // Handle arrays
  if (Array.isArray(value1)) {
    if (!Array.isArray(value2)) {
      return false;
    }

    const length = value1.length;

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

    for (let i=0; i < length; i++) {
      if (!areEquivalent(value1[i], value2[i], stack)) {
        return false;
      }
    }
    return true;
  }

  // Final case: object

  // get both key lists and check length
  const keys1 = Object.keys(value1);
  const keys2 = Object.keys(value2);
  const numKeys = keys1.length;

  if (keys2.length !== numKeys) {
    return false;
  }

  // Empty object on both sides?
  if (numKeys === 0) {
    return true;
  }

  // sort is a native call so it's very fast - much faster than comparing the
  // values at each key if it can be avoided, so do the sort and then
  // ensure every key matches at every index
  keys1.sort();
  keys2.sort();

  // Ensure perfect match across all keys
  for(let i = 0; i < numKeys; i++) {
    if (keys1[i] !== keys2[i]) {
      return false;
    }
  }

  // Ensure perfect match across all values
  for(let i = 0; i < numKeys; i++) {
    if (!areEquivalent(value1[keys1[i]], value2[keys1[i]], stack)) {
      return false;
    }
  }

  // back up
  stack.pop();

  // Walk the same, talk the same - matching ducks. Quack.
  // 🦆🦆
  return true;
}

@edwinro
Copy link

edwinro commented Mar 17, 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?

@nicbell
Copy link
Author

nicbell commented Jun 15, 2021

Always glad to see this old thing created in 2013 has got people talking and learning from each other.

@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