@feugene/mu
Version:
Helpful TS utilities without dependencies
118 lines (96 loc) • 3.14 kB
text/typescript
/**
* Compares the contents of 2 or more objects using strict equality.
*/
import isObject from '~/is/isObject'
export default function equal(origin: Record<PropertyKey, any>, ...list: Record<PropertyKey, any>[]): boolean {
let i: number, l: number, leftChain: Array<any>, rightChain: Array<any>
if (!isObject(origin) || list.length === 0) {
throw new Error('Need two or more arguments to compare')
}
function compare2Objects(x: any, y: any) {
let p
// remember that NaN === NaN returns false
// and isNaN(undefined) returns true
// isNumeric(x,y)
if (isNaN(x) && isNaN(y) && typeof x === 'number' && typeof y === 'number') {
return true
}
// Compare primitives and functions.
// Check if both arguments link to the same object.
// Especially useful on the step where we compare prototypes
if (x === y) {
return true
}
// Works in case when functions are created in constructor.
// Comparing dates is a common scenario. Another built-ins?
// We can even handle functions passed across iframes
if (
(typeof x === 'function' && typeof y === 'function') ||
(x instanceof Date && y instanceof Date) ||
(x instanceof RegExp && y instanceof RegExp) ||
(x instanceof String && y instanceof String) ||
(x instanceof Number && y instanceof Number)
) {
return x.toString() === y.toString()
}
// At last checking prototypes as good as we can
if (!(x instanceof Object && y instanceof Object)) {
return false
}
if (Object.prototype.isPrototypeOf.call(x, y) || Object.prototype.isPrototypeOf.call(y, x)) {
return false
}
if (x.constructor !== y.constructor) {
return false
}
if (x.prototype !== y.prototype) {
return false
}
// Check for infinitive linking loops
if (leftChain.includes(x) || rightChain.includes(y)) {
return false
}
// Quick checking of one object being a subset of another.
// todo: cache the structure of arguments[0] for performance
for (p in y) {
if (Object.prototype.isPrototypeOf.call(y, p) !== Object.prototype.isPrototypeOf.call(x, p)) {
return false
} else if (typeof y[p] !== typeof x[p]) {
return false
}
}
for (p in x) {
if (Object.prototype.isPrototypeOf.call(y, p) !== Object.prototype.isPrototypeOf.call(x, p)) {
return false
} else if (typeof y[p] !== typeof x[p]) {
return false
}
switch (typeof x[p]) {
case 'object':
case 'function':
leftChain.push(x)
rightChain.push(y)
if (!compare2Objects(x[p], y[p])) {
return false
}
leftChain.pop()
rightChain.pop()
break
default:
if (x[p] !== y[p]) {
return false
}
break
}
}
return true
}
for (i = 0, l = list.length; i < l; i++) {
leftChain = [] // @Todo: this can be cached
rightChain = []
if (!compare2Objects(origin, list[i])) {
return false
}
}
return true
}