-
-
Save nicbell/6081098 to your computer and use it in GitHub Desktop.
//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 |
is this what you're trying to make? https://ramdajs.com/docs/#equals
keeping simple:
Object.compare = (a, b) => JSON.stringify(a) === JSON.stringify(b);
It ignores functions, but work for most scenarios
keeping simple:
Object.compare = (a, b) => JSON.stringify(a) === JSON.stringify(b);
It ignores functions, but work for most scenarios
and fails to compare properly objects with the same fields in a different order
keeping simple:
Object.compare = (a, b) => JSON.stringify(a) === JSON.stringify(b);
It ignores functions, but work for most scenariosand fails to compare properly objects with the same fields in a different order
Solvable by doing:
Object.compare = (a, b) => JSON.stringify(Object.entries(a).sort()) === JSON.stringify(Object.entries(b).sort())
keeping simple:
Object.compare = (a, b) => JSON.stringify(a) === JSON.stringify(b);
It ignores functions, but work for most scenariosand fails to compare properly objects with the same fields in a different order
Solvable by doing:
Object.compare = (a, b) => JSON.stringify(Object.entries(a).sort()) === JSON.stringify(Object.entries(b).sort())
not really solve for objects with nested objects
keeping simple:
Object.compare = (a, b) => JSON.stringify(a) === JSON.stringify(b);
It ignores functions, but work for most scenariosand fails to compare properly objects with the same fields in a different order
Solvable by doing:
Object.compare = (a, b) => JSON.stringify(Object.entries(a).sort()) === JSON.stringify(Object.entries(b).sort())
not really solve for objects with nested objects
Solvable by doing:
Object.compare = (a, b) => { let s = o => Object.entries(o).sort().map(i => { if(i[1] instanceof Object) i[1] = s(i[1]); return i }) return JSON.stringify(s(a)) === JSON.stringify(s(b)) }
Not simple anymore =( *still quite smaller than the proposed solutions
Can you add a max call depth param please?
@quantuminformation this isn't a library it's just Gist. When you use it you are welcome to add a max call depth param.
Seems to fail with null values
@tannerjt this isn't a library it's just a Gist. When you use it you are welcome to add a null check.
Thanks, @nicbell. I added it to a project I was working on and just letting others know who attempt to use this and run into the same issue. Thanks for posting this as a GIST, very helpful.
If anyone is concerned about circular objects, I think this should solve this issue for most cases.
Object.compare = function(obj1, obj2) {
const top1 = obj1;
const top2 = obj2;
const run = (obj1, obj2) => {
// The rest of the original code...
switch(typeof obj1[p]) {
case "object":
if((Object.is(top1, obj1[p]) && Object.is(top2, obj2[p]))
|| (Object.is(top2, obj1[p]) && Object.is(top1, obj2[p]))
|| (Object.is(obj1, obj2))) continue;
if (!run(obj1[p], obj2[p])) return false;
break;
}
// More code...
}
return run(obj1, obj2);
}
For Internet Explorer there is a polyfill for Object.is() or this would probably work fine because they are just referencing the object at the top level.
switch(typeof obj1[p]) {
case "object":
if((top1 === obj1[p] && top2 === obj2[p])
|| (top2 === obj1[p] && top1 === obj2[p])
|| (obj1 === obj2)) continue;
if (!run(obj1[p], obj2[p])) return false;
break;
}
// etc ...
}
Class gist btw mate :) it's definitely inspired me
@dbuzzin glad you found it helpful. 🙏
Hey guys. This is unnecessary. We can only:
const foo = { a: 1, b: { single: 11 }};
const bar = {
a: 1,
b: {
single: 11
}
};
JSON.stringify(foo) === JSON.stringify(bar); // true
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;
}
};
function ObjectCompare(obj1, obj2) {
return !( obj1 < obj2 || obj1 > obj2);
}
Worked for me
@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;
@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;
}
};
@edwinro Thanks for the update. I snuck in an edit re. an issue with the nullish test which you understandably missed. ie. == vs ===
@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.
@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. 😀
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
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;
}
+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?
Always glad to see this old thing created in 2013 has got people talking and learning from each other.
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;
+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
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();
}
In the Object.compare method is absent the case of Array. If you compare two identical arrays with == or === the result will be false.
const arr1 = [1, 2, 3]; const arr2 = [1, 2, 3]; arr1 === arr2 // false