| # fast-equals |
|
|
| <img src="https://img.shields.io/badge/build-passing-brightgreen.svg"/> |
| <img src="https://img.shields.io/badge/coverage-100%25-brightgreen.svg"/> |
| <img src="https://img.shields.io/badge/license-MIT-blue.svg"/> |
|
|
| Perform [blazing fast](#benchmarks) 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 ~2kB when |
| minified and gzipped. |
|
|
| The following types are handled out-of-the-box: |
|
|
| - Plain objects (including `react` elements and `Arguments`) |
| - Arrays |
| - `ArrayBuffer` / `TypedArray` / `DataView` instances |
| - `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 |
|
|
| - [fast-equals](#fast-equals) |
| - [Table of contents](#table-of-contents) |
| - [Usage](#usage) |
| - [Specific builds](#specific-builds) |
| - [Available methods](#available-methods) |
| - [deepEqual](#deepequal) |
| - [Comparing `Map`s](#comparing-maps) |
| - [shallowEqual](#shallowequal) |
| - [sameValueZeroEqual](#samevaluezeroequal) |
| - [circularDeepEqual](#circulardeepequal) |
| - [circularShallowEqual](#circularshallowequal) |
| - [strictDeepEqual](#strictdeepequal) |
| - [strictShallowEqual](#strictshallowequal) |
| - [strictCircularDeepEqual](#strictcirculardeepequal) |
| - [strictCircularShallowEqual](#strictcircularshallowequal) |
| - [createCustomEqual](#createcustomequal) |
| - [unknownTagComparators](#unknowntagcomparators) |
| - [Recipes](#recipes) |
| - [Benchmarks](#benchmarks) |
| - [Development](#development) |
|
|
| ## Usage |
|
|
| ```ts |
| 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](https://github.com/planttheidea/fast-equals/issues). |
|
|
| ## Available methods |
|
|
| ### deepEqual |
|
|
| Performs a deep equality comparison on the two objects passed and returns a boolean representing the value equivalency |
| of the objects. |
|
|
| ```ts |
| 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 |
| ``` |
|
|
| #### Comparing `Map`s |
|
|
| `Map` objects support complex keys (objects, Arrays, etc.), however |
| [the spec for key lookups in `Map` are based on `SameZeroValue`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map#key_equality). |
| If the spec were followed for comparison, the following would always be `false`: |
|
|
| ```ts |
| 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. |
|
|
| ```ts |
| 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`](http://ecma-international.org/ecma-262/7.0/#sec-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`. |
|
|
| ```ts |
| 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. |
|
|
| ```ts |
| 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](#comparing-maps). |
|
|
| ### 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. |
|
|
| ```ts |
| 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 |
|
|
| ```ts |
| 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 |
|
|
| ```ts |
| 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 |
|
|
| ```ts |
| 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 |
|
|
| ```ts |
| 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: |
|
|
| ```ts |
| 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>; |
| areErrorsEqual: TypeEqualityComparator<Error, Meta>; |
| areFunctionsEqual: TypeEqualityComparator<(...args: any[]) => any, 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: TypeEqualityComparator<TypedArray, Meta>; |
| areUrlsEqual: TypeEqualityComparator<URL, Meta>; |
| unknownTagComparators: Record<string, TypeEqualityComparator<string, any>>; |
| } |
| |
| 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](#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](https://github.com/planttheidea/fast-equals/issues). |
|
|
| _**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._ |
|
|
| #### unknownTagComparators |
|
|
| If you want to compare objects that have a custom `@@toStringTag`, you can provide a map of the custom tags you want to |
| support via the `unknownTagComparators` option. See [this recipe]('./recipes/special-objects.md) for an example. |
|
|
| #### 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](./recipes/legacy-regexp-support.md) |
| - [Explicit property check](./recipes/explicit-property-check.md) |
| - [Using `meta` in comparison](./recipes//using-meta-in-comparison.md) |
| - [Comparing non-standard properties](./recipes/non-standard-properties.md) |
| - [Strict property descriptor comparison](./recipes/strict-property-descriptor-check.md) |
| - [Legacy environment support for circualr equal comparators](./recipes/legacy-circular-equal-support.md) |
| - [Custom `@@toStringTag` support](./recipes/special-objects.md) |
|
|
| ## Benchmarks |
|
|
| All benchmarks were performed on an i9-11900H Ubuntu Linux 24.04 laptop with 64GB of memory using NodeJS version |
| `20.17.0`, 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 |
|
|
| ```bash |
| Testing mixed objects equal... |
| ββββββββββββββββββββββββββββββββββββββββββ¬βββββββββββββββββ |
| β Name β Ops / sec β |
| ββββββββββββββββββββββββββββββββββββββββββΌβββββββββββββββββ€ |
| β fast-equals (passed) β 1416193.769468 β |
| ββββββββββββββββββββββββββββββββββββββββββΌβββββββββββββββββ€ |
| β fast-deep-equal (passed) β 1284824.583215 β |
| ββββββββββββββββββββββββββββββββββββββββββΌβββββββββββββββββ€ |
| β react-fast-compare (passed) β 1246947.505444 β |
| ββββββββββββββββββββββββββββββββββββββββββΌβββββββββββββββββ€ |
| β shallow-equal-fuzzy (passed) β 1238082.379207 β |
| ββββββββββββββββββββββββββββββββββββββββββΌβββββββββββββββββ€ |
| β nano-equal (failed) β 946782.33704 β |
| ββββββββββββββββββββββββββββββββββββββββββΌβββββββββββββββββ€ |
| β dequal/lite (passed) β 758213.632866 β |
| ββββββββββββββββββββββββββββββββββββββββββΌβββββββββββββββββ€ |
| β dequal (passed) β 756789.655029 β |
| ββββββββββββββββββββββββββββββββββββββββββΌβββββββββββββββββ€ |
| β fast-equals (circular) (passed) β 726093.253185 β |
| ββββββββββββββββββββββββββββββββββββββββββΌβββββββββββββββββ€ |
| β underscore.isEqual (passed) β 489748.701783 β |
| ββββββββββββββββββββββββββββββββββββββββββΌβββββββββββββββββ€ |
| β assert.deepStrictEqual (passed) β 453761.890107 β |
| ββββββββββββββββββββββββββββββββββββββββββΌβββββββββββββββββ€ |
| β lodash.isEqual (passed) β 288264.867811 β |
| ββββββββββββββββββββββββββββββββββββββββββΌβββββββββββββββββ€ |
| β fast-equals (strict) (passed) β 217221.619705 β |
| ββββββββββββββββββββββββββββββββββββββββββΌβββββββββββββββββ€ |
| β fast-equals (strict circular) (passed) β 186916.942934 β |
| ββββββββββββββββββββββββββββββββββββββββββΌβββββββββββββββββ€ |
| β deep-eql (passed) β 162487.877883 β |
| ββββββββββββββββββββββββββββββββββββββββββΌβββββββββββββββββ€ |
| β deep-equal (passed) β 916.680714 β |
| ββββββββββββββββββββββββββββββββββββββββββ΄βββββββββββββββββ |
| |
| Testing mixed objects not equal... |
| ββββββββββββββββββββββββββββββββββββββββββ¬βββββββββββββββββ |
| β Name β Ops / sec β |
| ββββββββββββββββββββββββββββββββββββββββββΌβββββββββββββββββ€ |
| β fast-equals (passed) β 4687012.640614 β |
| ββββββββββββββββββββββββββββββββββββββββββΌβββββββββββββββββ€ |
| β fast-deep-equal (passed) β 3418170.156109 β |
| ββββββββββββββββββββββββββββββββββββββββββΌβββββββββββββββββ€ |
| β react-fast-compare (passed) β 3283516.669966 β |
| ββββββββββββββββββββββββββββββββββββββββββΌβββββββββββββββββ€ |
| β fast-equals (circular) (passed) β 3268062.099602 β |
| ββββββββββββββββββββββββββββββββββββββββββΌβββββββββββββββββ€ |
| β fast-equals (strict) (passed) β 1747578.66456 β |
| ββββββββββββββββββββββββββββββββββββββββββΌβββββββββββββββββ€ |
| β fast-equals (strict circular) (passed) β 1477873.624956 β |
| ββββββββββββββββββββββββββββββββββββββββββΌβββββββββββββββββ€ |
| β dequal/lite (passed) β 1335397.839502 β |
| ββββββββββββββββββββββββββββββββββββββββββΌβββββββββββββββββ€ |
| β dequal (passed) β 1319426.71146 β |
| ββββββββββββββββββββββββββββββββββββββββββΌβββββββββββββββββ€ |
| β shallow-equal-fuzzy (failed) β 1237432.986615 β |
| ββββββββββββββββββββββββββββββββββββββββββΌβββββββββββββββββ€ |
| β nano-equal (passed) β 1064383.319776 β |
| ββββββββββββββββββββββββββββββββββββββββββΌβββββββββββββββββ€ |
| β underscore.isEqual (passed) β 920462.516736 β |
| ββββββββββββββββββββββββββββββββββββββββββΌβββββββββββββββββ€ |
| β lodash.isEqual (passed) β 379370.998021 β |
| ββββββββββββββββββββββββββββββββββββββββββΌβββββββββββββββββ€ |
| β deep-eql (passed) β 184111.383127 β |
| ββββββββββββββββββββββββββββββββββββββββββΌβββββββββββββββββ€ |
| β assert.deepStrictEqual (passed) β 20775.59065 β |
| ββββββββββββββββββββββββββββββββββββββββββΌβββββββββββββββββ€ |
| β deep-equal (passed) β 3678.51009 β |
| ββββββββββββββββββββββββββββββββββββββββββ΄βββββββββββββββββ |
| ``` |
|
|
| Caveats that impact the benchmark (and accuracy of comparison): |
|
|
| - `Map`s, `Promise`s, and `Set`s were excluded from the benchmark entirely because no library other than `deep-eql` |
| fully supported their comparison |
| - `fast-deep-equal`, `react-fast-compare` and `nano-equal` throw on objects with `null` as prototype |
| (`Object.create(null)`) |
| - `assert.deepStrictEqual` does not support `NaN` or `SameValueZero` equality for dates |
| - `deep-eql` does not support `SameValueZero` equality for zero equality (positive and negative zero are not equal) |
| - `deep-equal` does not support `NaN` and does not strictly compare object type, or date / regexp values, nor uses |
| `SameValueZero` equality for dates |
| - `fast-deep-equal` does not support `NaN` or `SameValueZero` equality for dates |
| - `nano-equal` does not strictly compare object property structure, array length, or object type, nor `SameValueZero` |
| equality for dates |
| - `react-fast-compare` does not support `NaN` or `SameValueZero` equality for dates, and does not compare `function` |
| equality |
| - `shallow-equal-fuzzy` does not strictly compare object type or regexp values, nor `SameValueZero` equality for dates |
| - `underscore.isEqual` does not support `SameValueZero` 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`, and `browser` distributables with `rollup` |
| - clean => run `rimraf` on the `dist` folder |
| - dev => start `vite` playground App |
| - dist => run `build` |
| - lint => run ESLint on all files in `src` folder (also runs on `dev` script) |
| - lint:fix => run `lint` script, but with auto-fixer |
| - prepublish:compile => run `lint`, `test:coverage`, `transpile:lib`, `transpile:es`, and `dist` 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 via `nyc` |
| - test:watch => run same script as `test` but keep persistent watcher |
| |