Created
August 18, 2017 15:14
-
-
Save jasmith79/3fa6e391f47581823bcf04f461ec4c7c to your computer and use it in GitHub Desktop.
JavaScript Value Equality
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 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