UNPKG

fast-equals

Version:

A blazing-fast equality comparison utility for a variety of use-cases

537 lines (421 loc) • 26.1 kB
> fast-equals Perform [blazing fast](#benchmarks) equality comparisons between two objects, while also allowing for flexibility for various use-cases. It has no dependencies, and is ~2.46kB 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 and then-ables - `Error` objects, including subclasses and their own properties - `URL` objects - Primitive wrappers (`new Boolean()` / `new Number()` / `new String()`, and boxed `BigInt`) - Custom class instances, including subclasses of native classes Methods are available for deep, shallow, [`SameValue`](https://262.ecma-international.org/7.0/#sec-samevalue), [`SameValueZero`](https://262.ecma-international.org/7.0/#sec-samevaluezero), or [strict equality](https://262.ecma-international.org/7.0/#sec-strict-equality-comparison) 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. 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/es/index.mjs` - CommonJS => `fast-equals/dist/cjs/index.cjs` If you are having any problems, want to request a new feature, or have any questions, [please file an issue](https://github.com/planttheidea/fast-equals/issues). - [Usage](#usage) - [Available methods](#available-methods) - [deepEqual](#deepequal) - [Comparing `Map`s](#comparing-maps) - [shallowEqual](#shallowequal) - [sameValueEqual](#samevalueequal) - [sameValueZeroEqual](#samevaluezeroequal) - [strictEqual](#strictequal) - [circularDeepEqual](#circulardeepequal) - [circularShallowEqual](#circularshallowequal) - [strictDeepEqual](#strictdeepequal) - [strictShallowEqual](#strictshallowequal) - [strictCircularDeepEqual](#strictcirculardeepequal) - [strictCircularShallowEqual](#strictcircularshallowequal) - [createCustomEqual](#createcustomequal) - [strictNullPrototypeComparison](#strictnullprototypecomparison) - [getUnsupportedCustomComparator](#getunsupportedcustomcomparator) - [Recipes](#recipes) - [Benchmarks](#benchmarks) ## Usage ```ts import { deepEqual } from 'fast-equals'; console.log(deepEqual({ foo: 'bar' }, { foo: 'bar' })); // true ``` ## 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 `SameValueZero`](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 pairs. 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 ``` ### sameValueEqual Performs a [`SameValue`](https://262.ecma-international.org/7.0/#sec-samevalue) comparison on the two objects passed and returns a boolean representing the value equivalency of the objects. In simple terms, this means: - `+0` and `-0` are not equal - `NaN` is equal to `NaN` - All other items are based on referential equality (`a === b`) ```ts import { sameValueEqual } from 'fast-equals'; const mainObject = { foo: NaN, bar: 'baz' }; const objectA = 'baz'; const objectB = NaN; const objectC = { foo: NaN, bar: 'baz' }; console.log(sameValueEqual(mainObject.bar, objectA)); // true console.log(sameValueEqual(mainObject.foo, objectB)); // true console.log(sameValueEqual(mainObject, objectC)); // false ``` _**NOTE**: In environments that support [`Object.is`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is), `sameValueEqual` is just a re-export of that method._ ### sameValueZeroEqual Performs a [`SameValueZero`](https://262.ecma-international.org/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: - `+0` and `-0` are equal - `NaN` is equal to `NaN` - All other items are based on referential equality (`a === b`) ```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 ``` ### strictEqual Performs a [Strict Equality](https://262.ecma-international.org/7.0/#sec-strict-equality-comparison) comparison on the two objects passed and returns a boolean representing the referential equality of the objects. In simple terms, this means: - `+0` and `-0` are equal - `NaN` is not equal to `NaN` - All other items are based on referential equality (`a === b`) ```ts import { strictEqual } from 'fast-equals'; const mainObject = { foo: NaN, bar: 'baz' }; const objectA = 'baz'; const objectB = NaN; const objectC = { foo: NaN, bar: 'baz' }; console.log(strictEqual(mainObject.bar, objectA)); // true console.log(strictEqual(mainObject.foo, objectB)); // false console.log(strictEqual(mainObject, objectC)); // false ``` _**NOTE**: This is mainly a convenience function, such as needing a default functional equality comparator. Naturally, it is faster to simply compare `a === b`. :)_ ### 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. 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. 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(strictShallowEqual(array, otherArray)); // true console.log(strictShallowEqual(array, ['foo'])); // false ``` ### strictCircularDeepEqual Performs the same comparison as `circularDeepEqual` but performs a strict comparison of the objects. 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(strictCircularDeepEqual(first, second)); // true console.log(strictCircularDeepEqual(first, new Circular('foo'))); // false ``` ### strictCircularShallowEqual Performs the same comparison as `circularShallowEqual` but performs a strict comparison of the objects. 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(array); // Shallow comparison requires the same nested reference. array.bar = 'baz'; otherArray.bar = 'baz'; console.log(strictCircularShallowEqual(array, otherArray)); // true console.log(strictCircularShallowEqual(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> { areArrayBuffersEqual: EqualityComparator<Meta>; areArraysEqual: EqualityComparator<Meta>; areDataViewsEqual: EqualityComparator<Meta>; areDatesEqual: EqualityComparator<Meta>; areErrorsEqual: EqualityComparator<Meta>; areFunctionsEqual: EqualityComparator<Meta>; areMapsEqual: EqualityComparator<Meta>; areNumbersEqual: EqualityComparator<Meta>; areObjectsEqual: EqualityComparator<Meta>; arePrimitiveWrappersEqual: EqualityComparator<Meta>; areRegExpsEqual: EqualityComparator<Meta>; areSetsEqual: EqualityComparator<Meta>; areTypedArraysEqual: EqualityComparator<Meta>; areUrlsEqual: EqualityComparator<Meta>; getUnsupportedCustomComparator: (<Type>(a: Type, b: Type, state: State<Meta>, tag: string) => EqualityComparator<Meta> | undefined) | undefined; strictNullPrototypeComparison?: boolean; } 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; strictNullPrototypeComparison?: 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._ #### strictNullPrototypeComparison Defaults to `true`, which keeps a null-prototype object (`Object.create(null)`) unequal to an otherwise identical plain object. Set it to `false` to compare the two as dictionaries, for values such as [ProseMirror node attributes](https://prosemirror.net/docs/ref/#model.Node.attrs) or `Object.groupBy` results. ```ts const equal = createCustomEqual({ strictNullPrototypeComparison: false }); const attrs = Object.assign(Object.create(null), { aspectRatio: 16 / 9 }); equal(attrs, { ...attrs }); // true ``` This is the only exception to the constructor check: a null-prototype object remains unequal to arrays, `Map`s, dates, and other built-ins, and distinct class constructors remain unequal to each other. It applies to nested values and can be combined with `circular`, `strict`, and custom comparators. #### getUnsupportedCustomComparator If you want to compare objects whose tag is not handled out-of-the-box (such as a custom `Symbol.toStringTag` or a `WeakMap`), you can provide a `getUnsupportedCustomComparator` method. It receives both values, the comparison state, and the tag, and returns the comparator to use for them. Returning `undefined` treats the values as unequal. 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) - [Legacy environment support for circular equal comparators](./recipes/legacy-circular-equal-support.md) - [Custom comparator 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 `24.15.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) │ 1522774.1567 │ ├────────────────────────────────────────┼────────────────┤ │ react-fast-compare (passed) │ 1285289.880042 │ ├────────────────────────────────────────┼────────────────┤ │ fast-deep-equal (passed) │ 1285105.414715 │ ├────────────────────────────────────────┼────────────────┤ │ shallow-equal-fuzzy (passed) │ 1248325.039481 │ ├────────────────────────────────────────┼────────────────┤ │ nano-equal (failed) │ 962152.688616 │ ├────────────────────────────────────────┼────────────────┤ │ fast-equals (circular) (passed) │ 800100.693495 │ ├────────────────────────────────────────┼────────────────┤ │ dequal/lite (passed) │ 778011.650869 │ ├────────────────────────────────────────┼────────────────┤ │ dequal (passed) │ 770187.208947 │ ├────────────────────────────────────────┼────────────────┤ │ underscore.isEqual (passed) │ 449433.41171 │ ├────────────────────────────────────────┼────────────────┤ │ assert.deepStrictEqual (passed) │ 442249.829162 │ ├────────────────────────────────────────┼────────────────┤ │ es-toolkit (passed) │ 355943.82757 │ ├────────────────────────────────────────┼────────────────┤ │ fast-equals (strict) (passed) │ 307931.96677 │ ├────────────────────────────────────────┼────────────────┤ │ lodash.isEqual (passed) │ 294660.269815 │ ├────────────────────────────────────────┼────────────────┤ │ fast-equals (strict circular) (passed) │ 252760.794458 │ ├────────────────────────────────────────┼────────────────┤ │ deep-eql (passed) │ 166131.383227 │ ├────────────────────────────────────────┼────────────────┤ │ deep-equal (passed) │ 970.944208 │ └────────────────────────────────────────┴────────────────┘ Testing mixed objects not equal... ┌────────────────────────────────────────┬────────────────┐ │ Name │ Ops / sec │ ├────────────────────────────────────────┼────────────────┤ │ fast-equals (passed) │ 5122472.769802 │ ├────────────────────────────────────────┼────────────────┤ │ fast-deep-equal (passed) │ 3520297.242957 │ ├────────────────────────────────────────┼────────────────┤ │ fast-equals (circular) (passed) │ 3485912.9348 │ ├────────────────────────────────────────┼────────────────┤ │ react-fast-compare (passed) │ 3414203.488651 │ ├────────────────────────────────────────┼────────────────┤ │ fast-equals (strict) (passed) │ 3038182.032793 │ ├────────────────────────────────────────┼────────────────┤ │ fast-equals (strict circular) (passed) │ 2344879.08551 │ ├────────────────────────────────────────┼────────────────┤ │ dequal/lite (passed) │ 1340883.333611 │ ├────────────────────────────────────────┼────────────────┤ │ dequal (passed) │ 1311832.728093 │ ├────────────────────────────────────────┼────────────────┤ │ shallow-equal-fuzzy (failed) │ 1249967.301358 │ ├────────────────────────────────────────┼────────────────┤ │ nano-equal (passed) │ 1096529.56672 │ ├────────────────────────────────────────┼────────────────┤ │ underscore.isEqual (passed) │ 660524.165237 │ ├────────────────────────────────────────┼────────────────┤ │ lodash.isEqual (passed) │ 385500.174796 │ ├────────────────────────────────────────┼────────────────┤ │ es-toolkit (passed) │ 365617.977684 │ ├────────────────────────────────────────┼────────────────┤ │ deep-eql (passed) │ 186388.440331 │ ├────────────────────────────────────────┼────────────────┤ │ assert.deepStrictEqual (passed) │ 21060.162935 │ ├────────────────────────────────────────┼────────────────┤ │ deep-equal (passed) │ 3819.218852 │ └────────────────────────────────────────┴────────────────┘ ``` 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 `SameValue` equality for dates - `deep-eql` does not support `SameValue` 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 `SameValue` equality for dates - `fast-deep-equal` does not support `NaN` or `SameValue` equality for dates - `nano-equal` does not strictly compare object property structure, array length, or object type, nor `SameValue` equality for dates - `react-fast-compare` does not support `NaN` or `SameValue` equality for dates, and does not compare `function` equality - `shallow-equal-fuzzy` does not strictly compare object type or regexp values, nor `SameValue` equality for dates - `underscore.isEqual` does not support `SameValue` 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.