Skip to content

Instantly share code, notes, and snippets.

@jasmith79
Created August 18, 2017 15:14
Show Gist options
  • Save jasmith79/3fa6e391f47581823bcf04f461ec4c7c to your computer and use it in GitHub Desktop.
Save jasmith79/3fa6e391f47581823bcf04f461ec4c7c to your computer and use it in GitHub Desktop.
JavaScript Value Equality
/*
* Checks to items to see if they are equal in terms of value. For primitives
* that's easy with one exception, but reference types are harder.
*
* For the purposes of this gist I've defined equality for reference types as
* follows:
*
* Regexes, dates, and functions are equal if their string representations are
* equal.
* WeakMaps, WeakSets, and Generators are equal only if they are referentially
* equal, as their value semantics are unclear.
* Sets are equal if they contain the same values.
* Arrays, DOMTokenLists, NodeLists, and the arguments psuedo-object are equal
* if they have the same values at the same indicies.
* Maps and POJOs are equal if they have the exactly same key/value pairs.
* HTML Elements are equal if their innerHTMLs and tag names are equal.
*
* Also the exception that I mentioned above for primitive values:
* NaN is equal to NaN. There may be some advantages in IEEE 754 to the standard
* behavior, but it makes recursively testing nested structures for equality
* difficult.
*
* The point of all of this is to show that there needs to be a **standard**
* solution to this problem since my non-comprehensive ad-hoc one was over 60
* LoC and would require another 100+ to test properly.
*/
// Helper, extracts the hidden internal [[Class]] slot of a JavaScript object.
const extractHiddenClass = (r => a => {
return Object.prototype.toString.call(a).match(r)[1];
})(/ ([a-z]+)]$/i);
const trueEquals = (a, b) => {
if (a === b) return true;
if (typeof a !== typeof b) return false;
let aType = extractHiddenClass(a);
let bType = extractHiddenClass(b);
if (aType !== bType) return false;
switch (aType) {
// All primtive types except Number should already have exited. Only need
// to worry about reference types and NaN.
case 'Number':
// every other possibility is already covered.
return Number.isNaN(a) && Number.isNaN(b);
// NOTE: comparing string equality on functions can fail in certain rare
// cases involving obscure or old platforms (e.g. PlayStation 3's browser).
case 'RegExp':
case 'Date':
case 'Function':
case 'GeneratorFunction':
return a.toString() === b.toString();
// no good method for checking equality beyond ref
case 'WeakMap':
case 'WeakSet':
case 'Generator':
return false;
// need these only for cross-realm
case 'Math':
case 'JSON':
return true;
// NOTE: recursive, can potentially blow the stack on deeply nested
// structures.
case 'Array':
case 'NodeList':
case 'DOMTokenList':
if (a.length !== b.length) return false;
return a.every((x, i) => trueEquals(x, b[i]));
case 'Set':
case 'Map':
case 'Arguments':
return trueEquals(Array.from(a).sort(), Array.from(b).sort());
case 'Object':
return trueEquals(Object.entries(a).sort(), Object.entries(b).sort());
default:
// Check for HTMLElement
if (aType.match(/html/i)) {
return a.innerHTML === b.innerHTML &&
a.tagName === b.tagName &&
trueEquals(a.classList, b.classList);
}
return false;
}
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment