fast-equals
Perform blazing fast equality comparisons (either deep or shallow) on two objects passed, while also maintaining a high degree of flexibility for various implementation use-cases. It has no dependencies, and is ~1.8kB when minified and gzipped.
The following types are handled out-of-the-box:
- Plain objects (including
react
elements andArguments
) - Arrays
- Typed Arrays
-
Date
objects -
RegExp
objects -
Map
/Set
iterables -
Promise
objects - Primitive wrappers (
new Boolean()
/new Number()
/new String()
) - Custom class instances, including subclasses of native classes
Methods are available for deep, shallow, or referential equality comparison. In addition, you can opt into support for circular objects, or performing a "strict" comparison with unconventional property definition, or both. You can also customize any specific type comparison based on your application's use-cases.
Table of contents
Usage
import { deepEqual } from 'fast-equals';
console.log(deepEqual({ foo: 'bar' }, { foo: 'bar' })); // true
Specific builds
By default, npm should resolve the correct build of the package based on your consumption (ESM vs CommonJS). However, if you want to force use of a specific build, they can be located here:
- ESM =>
fast-equals/dist/esm/index.mjs
- CommonJS =>
fast-equals/dist/cjs/index.cjs
- UMD =>
fast-equals/dist/umd/index.js
- Minified UMD =>
fast-equals/dist/min/index.js
If you are having issues loading a specific build type, please file an issue.
Available methods
deepEqual
Performs a deep equality comparison on the two objects passed and returns a boolean representing the value equivalency of the objects.
import { deepEqual } from 'fast-equals';
const objectA = { foo: { bar: 'baz' } };
const objectB = { foo: { bar: 'baz' } };
console.log(objectA === objectB); // false
console.log(deepEqual(objectA, objectB)); // true
Map
s
Comparing Map
objects support complex keys (objects, Arrays, etc.), however the spec for key lookups in Map
are based on SameZeroValue
. If the spec were followed for comparison, the following would always be false
:
const mapA = new Map([[{ foo: 'bar' }, { baz: 'quz' }]]);
const mapB = new Map([[{ foo: 'bar' }, { baz: 'quz' }]]);
deepEqual(mapA, mapB);
To support true deep equality of all contents, fast-equals
will perform a deep equality comparison for key and value parirs. Therefore, the above would be true
.
shallowEqual
Performs a shallow equality comparison on the two objects passed and returns a boolean representing the value equivalency of the objects.
import { shallowEqual } from 'fast-equals';
const nestedObject = { bar: 'baz' };
const objectA = { foo: nestedObject };
const objectB = { foo: nestedObject };
const objectC = { foo: { bar: 'baz' } };
console.log(objectA === objectB); // false
console.log(shallowEqual(objectA, objectB)); // true
console.log(shallowEqual(objectA, objectC)); // false
sameValueZeroEqual
Performs a SameValueZero
comparison on the two objects passed and returns a boolean representing the value equivalency of the objects. In simple terms, this means either strictly equal or both NaN
.
import { sameValueZeroEqual } from 'fast-equals';
const mainObject = { foo: NaN, bar: 'baz' };
const objectA = 'baz';
const objectB = NaN;
const objectC = { foo: NaN, bar: 'baz' };
console.log(sameValueZeroEqual(mainObject.bar, objectA)); // true
console.log(sameValueZeroEqual(mainObject.foo, objectB)); // true
console.log(sameValueZeroEqual(mainObject, objectC)); // false
circularDeepEqual
Performs the same comparison as deepEqual
but supports circular objects. It is slower than deepEqual
, so only use if you know circular objects are present.
function Circular(value) {
this.me = {
deeply: {
nested: {
reference: this,
},
},
value,
};
}
console.log(circularDeepEqual(new Circular('foo'), new Circular('foo'))); // true
console.log(circularDeepEqual(new Circular('foo'), new Circular('bar'))); // false
Just as with deepEqual
, both keys and values are compared for deep equality.
circularShallowEqual
Performs the same comparison as shallowequal
but supports circular objects. It is slower than shallowEqual
, so only use if you know circular objects are present.
const array = ['foo'];
array.push(array);
console.log(circularShallowEqual(array, ['foo', array])); // true
console.log(circularShallowEqual(array, [array])); // false
strictDeepEqual
Performs the same comparison as deepEqual
but performs a strict comparison of the objects. In this includes:
- Checking symbol properties
- Checking non-enumerable properties in object comparisons
- Checking full descriptor of properties on the object to match
- Checking non-index properties on arrays
- Checking non-key properties on
Map
/Set
objects
const array = [{ foo: 'bar' }];
const otherArray = [{ foo: 'bar' }];
array.bar = 'baz';
otherArray.bar = 'baz';
console.log(strictDeepEqual(array, otherArray)); // true;
console.log(strictDeepEqual(array, [{ foo: 'bar' }])); // false;
strictShallowEqual
Performs the same comparison as shallowEqual
but performs a strict comparison of the objects. In this includes:
- Checking non-enumerable properties in object comparisons
- Checking full descriptor of properties on the object to match
- Checking non-index properties on arrays
- Checking non-key properties on
Map
/Set
objects
const array = ['foo'];
const otherArray = ['foo'];
array.bar = 'baz';
otherArray.bar = 'baz';
console.log(strictDeepEqual(array, otherArray)); // true;
console.log(strictDeepEqual(array, ['foo'])); // false;
strictCircularDeepEqual
Performs the same comparison as circularDeepEqual
but performs a strict comparison of the objects. In this includes:
- Checking
Symbol
properties on the object - Checking non-enumerable properties in object comparisons
- Checking full descriptor of properties on the object to match
- Checking non-index properties on arrays
- Checking non-key properties on
Map
/Set
objects
function Circular(value) {
this.me = {
deeply: {
nested: {
reference: this,
},
},
value,
};
}
const first = new Circular('foo');
Object.defineProperty(first, 'bar', {
enumerable: false,
value: 'baz',
});
const second = new Circular('foo');
Object.defineProperty(second, 'bar', {
enumerable: false,
value: 'baz',
});
console.log(circularDeepEqual(first, second)); // true
console.log(circularDeepEqual(first, new Circular('foo'))); // false
strictCircularShallowEqual
Performs the same comparison as circularShallowEqual
but performs a strict comparison of the objects. In this includes:
- Checking non-enumerable properties in object comparisons
- Checking full descriptor of properties on the object to match
- Checking non-index properties on arrays
- Checking non-key properties on
Map
/Set
objects
const array = ['foo'];
const otherArray = ['foo'];
array.push(array);
otherArray.push(otherArray);
array.bar = 'baz';
otherArray.bar = 'baz';
console.log(circularShallowEqual(array, otherArray)); // true
console.log(circularShallowEqual(array, ['foo', array])); // false
createCustomEqual
Creates a custom equality comparator that will be used on nested values in the object. Unlike deepEqual
and shallowEqual
, this is a factory method that receives the default options used internally, and allows you to override the defaults as needed. This is generally for extreme edge-cases, or supporting legacy environments.
The signature is as follows:
interface Cache<Key extends object, Value> {
delete(key: Key): boolean;
get(key: Key): Value | undefined;
set(key: Key, value: any): any;
}
interface ComparatorConfig<Meta> {
areArraysEqual: TypeEqualityComparator<any[], Meta>;
areDatesEqual: TypeEqualityComparator<Date, Meta>;
areMapsEqual: TypeEqualityComparator<Map<any, any>, Meta>;
areObjectsEqual: TypeEqualityComparator<Record<string, any>, Meta>;
arePrimitiveWrappersEqual: TypeEqualityComparator<
boolean | string | number,
Meta
>;
areRegExpsEqual: TypeEqualityComparator<RegExp, Meta>;
areSetsEqual: TypeEqualityComparator<Set<any>, Meta>;
areTypedArraysEqual: TypeEqualityComparatory<TypedArray, Meta>;
}
function createCustomEqual<Meta>(options: {
circular?: boolean;
createCustomConfig?: (
defaultConfig: ComparatorConfig<Meta>,
) => Partial<ComparatorConfig<Meta>>;
createInternalComparator?: (
compare: <A, B>(a: A, b: B, state: State<Meta>) => boolean,
) => (
a: any,
b: any,
indexOrKeyA: any,
indexOrKeyB: any,
parentA: any,
parentB: any,
state: State<Meta>,
) => boolean;
createState?: () => { cache?: Cache; meta?: Meta };
strict?: boolean;
}): <A, B>(a: A, b: B) => boolean;
Create a custom equality comparator. This allows complete control over building a bespoke equality method, in case your use-case requires a higher degree of performance, legacy environment support, or any other non-standard usage. The recipes provide examples of use in different use-cases, but if you have a specific goal in mind and would like assistance feel free to file an issue.
NOTE: Map
implementations compare equality for both keys and value. When using a custom comparator and comparing equality of the keys, the iteration index is provided as both indexOrKeyA
and indexOrKeyB
to help use-cases where ordering of keys matters to equality.
Recipes
Some recipes have been created to provide examples of use-cases for createCustomEqual
. Even if not directly applicable to the problem you are solving, they can offer guidance of how to structure your solution.
- Legacy environment support for
RegExp
comparators - Explicit property check
- Using
meta
in comparison - Comparing non-standard properties
- Strict property descriptor comparison
- Legacy environment support for circualr equal comparators
Benchmarks
All benchmarks were performed on an i9-11900H Ubuntu Linux 22.04 laptop with 64GB of memory using NodeJS version 16.14.2
, and are based on averages of running comparisons based deep equality on the following object types:
- Primitives (
String
,Number
,null
,undefined
) Function
Object
Array
Date
RegExp
-
react
elements - A mixed object with a combination of all the above types
Testing mixed objects equal...
┌─────────┬─────────────────────────────────┬────────────────┐
│ (index) │ Package │ Ops/sec │
├─────────┼─────────────────────────────────┼────────────────┤
│ 0 │ 'fast-equals' │ 1249567.730326 │
│ 1 │ 'fast-deep-equal' │ 1182463.587514 │
│ 2 │ 'react-fast-compare' │ 1152487.319161 │
│ 3 │ 'shallow-equal-fuzzy' │ 1092360.712389 │
│ 4 │ 'fast-equals (circular)' │ 676669.92003 │
│ 5 │ 'underscore.isEqual' │ 429430.837497 │
│ 6 │ 'lodash.isEqual' │ 237915.684734 │
│ 7 │ 'fast-equals (strict)' │ 181386.38032 │
│ 8 │ 'fast-equals (strict circular)' │ 156779.745875 │
│ 9 │ 'deep-eql' │ 139155.099209 │
│ 10 │ 'deep-equal' │ 1026.527229 │
└─────────┴─────────────────────────────────┴────────────────┘
Testing mixed objects not equal...
┌─────────┬─────────────────────────────────┬────────────────┐
│ (index) │ Package │ Ops/sec │
├─────────┼─────────────────────────────────┼────────────────┤
│ 0 │ 'fast-equals' │ 3255824.097237 │
│ 1 │ 'react-fast-compare' │ 2654721.726058 │
│ 2 │ 'fast-deep-equal' │ 2582218.974752 │
│ 3 │ 'fast-equals (circular)' │ 2474303.26566 │
│ 4 │ 'fast-equals (strict)' │ 1088066.604881 │
│ 5 │ 'fast-equals (strict circular)' │ 949253.614181 │
│ 6 │ 'nano-equal' │ 939170.554148 │
│ 7 │ 'underscore.isEqual' │ 738852.197879 │
│ 8 │ 'lodash.isEqual' │ 307306.622212 │
│ 9 │ 'deep-eql' │ 156250.110401 │
│ 10 │ 'assert.deepStrictEqual' │ 22839.454561 │
│ 11 │ 'deep-equal' │ 4034.45114 │
└─────────┴─────────────────────────────────┴────────────────┘
Caveats that impact the benchmark (and accuracy of comparison):
-
Map
s,Promise
s, andSet
s were excluded from the benchmark entirely because no library other thandeep-eql
fully supported their comparison -
fast-deep-equal
,react-fast-compare
andnano-equal
throw on objects withnull
as prototype (Object.create(null)
) -
assert.deepStrictEqual
does not supportNaN
orSameValueZero
equality for dates -
deep-eql
does not supportSameValueZero
equality for zero equality (positive and negative zero are not equal) -
deep-equal
does not supportNaN
and does not strictly compare object type, or date / regexp values, nor usesSameValueZero
equality for dates -
fast-deep-equal
does not supportNaN
orSameValueZero
equality for dates -
nano-equal
does not strictly compare object property structure, array length, or object type, norSameValueZero
equality for dates -
react-fast-compare
does not supportNaN
orSameValueZero
equality for dates, and does not comparefunction
equality -
shallow-equal-fuzzy
does not strictly compare object type or regexp values, norSameValueZero
equality for dates -
underscore.isEqual
does not supportSameValueZero
equality for primitives or dates
All of these have the potential of inflating the respective library's numbers in comparison to fast-equals
, but it was the closest apples-to-apples comparison I could create of a reasonable sample size. It should be noted that react
elements can be circular objects, however simple elements are not; I kept the react
comparison very basic to allow it to be included.
Development
Standard practice, clone the repo and npm i
to get the dependencies. The following npm scripts are available:
- benchmark => run benchmark tests against other equality libraries
- build => build
main
,module
, andbrowser
distributables withrollup
- clean => run
rimraf
on thedist
folder - dev => start webpack playground App
- dist => run
build
- lint => run ESLint on all files in
src
folder (also runs ondev
script) - lint:fix => run
lint
script, but with auto-fixer - prepublish:compile => run
lint
,test:coverage
,transpile:lib
,transpile:es
, anddist
scripts - start => run
dev
- test => run AVA with NODE_ENV=test on all files in
test
folder - test:coverage => run same script as
test
with code coverage calculation vianyc
- test:watch => run same script as
test
but keep persistent watcher