-
-
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 |
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();
}
will fail for dates as the value of any object