fast-equals 6.0.0-beta.1 → 6.0.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -34,7 +34,7 @@ customize any specific type comparison based on your application's use-cases.
34
34
  - [deepEqual](#deepequal)
35
35
  - [Comparing `Map`s](#comparing-maps)
36
36
  - [shallowEqual](#shallowequal)
37
- - [sameValueZeroEqual](#samevaluezeroequal)
37
+ - [sameValueEqual](#samevalueequal)
38
38
  - [circularDeepEqual](#circulardeepequal)
39
39
  - [circularShallowEqual](#circularshallowequal)
40
40
  - [strictDeepEqual](#strictdeepequal)
@@ -42,7 +42,7 @@ customize any specific type comparison based on your application's use-cases.
42
42
  - [strictCircularDeepEqual](#strictcirculardeepequal)
43
43
  - [strictCircularShallowEqual](#strictcircularshallowequal)
44
44
  - [createCustomEqual](#createcustomequal)
45
- - [unknownTagComparators](#unknowntagcomparators)
45
+ - [getUnsupportedCustomComparator](#getunsupportedcustomcomparator)
46
46
  - [Recipes](#recipes)
47
47
  - [Benchmarks](#benchmarks)
48
48
  - [Development](#development)
@@ -120,14 +120,44 @@ console.log(shallowEqual(objectA, objectB)); // true
120
120
  console.log(shallowEqual(objectA, objectC)); // false
121
121
  ```
122
122
 
123
+ ### sameValueEqual
124
+
125
+ Performs a [`SameValue`](http://ecma-international.org/ecma-262/7.0/#sec-samevalue) comparison on the two objects passed
126
+ and returns a boolean representing the value equivalency of the objects. In simple terms, this means:
127
+
128
+ - `+0` and `-0` are not equal
129
+ - `NaN` is equal to `NaN`
130
+ - All other items are based on referential equality (`a === b`)
131
+
132
+ ```ts
133
+ import { sameValueEqual } from 'fast-equals';
134
+
135
+ const mainObject = { foo: NaN, bar: 'baz' };
136
+
137
+ const objectA = 'baz';
138
+ const objectB = NaN;
139
+ const objectC = { foo: NaN, bar: 'baz' };
140
+
141
+ console.log(sameValueEqual(mainObject.bar, objectA)); // true
142
+ console.log(sameValueEqual(mainObject.foo, objectB)); // true
143
+ console.log(sameValueEqual(mainObject, objectC)); // false
144
+ ```
145
+
146
+ **NOTE**: In environments that support
147
+ [`Object.is`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is),
148
+ `sameValueEqual` is just a re-export of that method.
149
+
123
150
  ### sameValueZeroEqual
124
151
 
125
152
  Performs a [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero) comparison on the two
126
- objects passed and returns a boolean representing the value equivalency of the objects. In simple terms, this means
127
- either strictly equal or both `NaN`.
153
+ objects passed and returns a boolean representing the value equivalency of the objects. In simple terms, this means:
154
+
155
+ - `+0` and `-0` are equal
156
+ - `NaN` is equal to `NaN`
157
+ - All other items are based on referential equality (`a === b`)
128
158
 
129
159
  ```ts
130
- import { sameValueZeroEqual } from 'fast-equals';
160
+ import { sameValueEqual } from 'fast-equals';
131
161
 
132
162
  const mainObject = { foo: NaN, bar: 'baz' };
133
163
 
@@ -135,9 +165,9 @@ const objectA = 'baz';
135
165
  const objectB = NaN;
136
166
  const objectC = { foo: NaN, bar: 'baz' };
137
167
 
138
- console.log(sameValueZeroEqual(mainObject.bar, objectA)); // true
139
- console.log(sameValueZeroEqual(mainObject.foo, objectB)); // true
140
- console.log(sameValueZeroEqual(mainObject, objectC)); // false
168
+ console.log(sameValueEqual(mainObject.bar, objectA)); // true
169
+ console.log(sameValueEqual(mainObject.foo, objectB)); // true
170
+ console.log(sameValueEqual(mainObject, objectC)); // false
141
171
  ```
142
172
 
143
173
  ### circularDeepEqual
@@ -298,18 +328,21 @@ interface Cache<Key extends object, Value> {
298
328
  }
299
329
 
300
330
  interface ComparatorConfig<Meta> {
301
- areArraysEqual: TypeEqualityComparator<any[], Meta>;
331
+ areArrayBuffersEcqual: TypeEqualityComparator<ArrayBuffer, Meta>;
332
+ areArraysEcqual: TypeEqualityComparator<any[], Meta>;
333
+ areDataViewsEqual: TypeEqualityComparator<DataView, Meta>;
302
334
  areDatesEqual: TypeEqualityComparator<Date, Meta>;
303
335
  areErrorsEqual: TypeEqualityComparator<Error, Meta>;
304
336
  areFunctionsEqual: TypeEqualityComparator<(...args: any[]) => any, Meta>;
305
337
  areMapsEqual: TypeEqualityComparator<Map<any, any>, Meta>;
338
+ areNumbersEqual: TypeEqualityComparator<number, Meta>;
306
339
  areObjectsEqual: TypeEqualityComparator<Record<string, any>, Meta>;
307
- arePrimitiveWrappersEqual: TypeEqualityComparator<boolean | string | number, Meta>;
340
+ arePrimitiveWrappersEqual: TypeEqualityComparator<Boolean | Number | String, Meta>;
308
341
  areRegExpsEqual: TypeEqualityComparator<RegExp, Meta>;
309
342
  areSetsEqual: TypeEqualityComparator<Set<any>, Meta>;
310
343
  areTypedArraysEqual: TypeEqualityComparator<TypedArray, Meta>;
311
344
  areUrlsEqual: TypeEqualityComparator<URL, Meta>;
312
- unknownTagComparators: Record<string, TypeEqualityComparator<string, any>>;
345
+ getUnsupportedCustomComparator: <Type>(a: Type, b: Type, tag: string) => TypeEqualityComparator<Type, Meta>;
313
346
  }
314
347
 
315
348
  function createCustomEqual<Meta>(options: {
@@ -332,10 +365,11 @@ _**NOTE**: `Map` implementations compare equality for both keys and value. When
332
365
  equality of the keys, the iteration index is provided as both `indexOrKeyA` and `indexOrKeyB` to help use-cases where
333
366
  ordering of keys matters to equality._
334
367
 
335
- #### unknownTagComparators
368
+ #### getUnsupportedCustomComparator
336
369
 
337
370
  If you want to compare objects that have a custom `@@toStringTag`, you can provide a map of the custom tags you want to
338
- support via the `unknownTagComparators` option. See [this recipe]('./recipes/special-objects.md) for an example.
371
+ support via the `getUnsupportedCustomComparator` option. See [this recipe]('./recipes/special-objects.md) for an
372
+ example.
339
373
 
340
374
  #### Recipes
341
375
 
@@ -348,12 +382,12 @@ to the problem you are solving, they can offer guidance of how to structure your
348
382
  - [Comparing non-standard properties](./recipes/non-standard-properties.md)
349
383
  - [Strict property descriptor comparison](./recipes/strict-property-descriptor-check.md)
350
384
  - [Legacy environment support for circualr equal comparators](./recipes/legacy-circular-equal-support.md)
351
- - [Custom `@@toStringTag` support](./recipes/special-objects.md)
385
+ - [Custom comparator support](./recipes/special-objects.md)
352
386
 
353
387
  ## Benchmarks
354
388
 
355
389
  All benchmarks were performed on an i9-11900H Ubuntu Linux 24.04 laptop with 64GB of memory using NodeJS version
356
- `20.17.0`, and are based on averages of running comparisons based deep equality on the following object types:
390
+ `24.11.1`, and are based on averages of running comparisons based deep equality on the following object types:
357
391
 
358
392
  - Primitives (`String`, `Number`, `null`, `undefined`)
359
393
  - `Function`
@@ -365,74 +399,73 @@ All benchmarks were performed on an i9-11900H Ubuntu Linux 24.04 laptop with 64G
365
399
  - A mixed object with a combination of all the above types
366
400
 
367
401
  ```bash
368
- Testing mixed objects equal...
369
402
  ┌────────────────────────────────────────┬────────────────┐
370
403
  │ Name │ Ops / sec │
371
404
  ├────────────────────────────────────────┼────────────────┤
372
- │ fast-equals (passed) │ 1416193.769468
405
+ │ fast-equals (passed) │ 1544237.29413
373
406
  ├────────────────────────────────────────┼────────────────┤
374
- │ fast-deep-equal (passed) │ 1284824.583215
407
+ │ fast-deep-equal (passed) │ 1328583.767745
375
408
  ├────────────────────────────────────────┼────────────────┤
376
- │ react-fast-compare (passed) │ 1246947.505444
409
+ │ react-fast-compare (passed) │ 1301727.296375
377
410
  ├────────────────────────────────────────┼────────────────┤
378
- │ shallow-equal-fuzzy (passed) │ 1238082.379207
411
+ │ shallow-equal-fuzzy (passed) │ 1225981.400919
379
412
  ├────────────────────────────────────────┼────────────────┤
380
- │ nano-equal (failed) │ 946782.33704
413
+ │ nano-equal (failed) │ 969495.538753
381
414
  ├────────────────────────────────────────┼────────────────┤
382
- dequal/lite (passed) 758213.632866
415
+ fast-equals (circular) (passed) 813716.49516
383
416
  ├────────────────────────────────────────┼────────────────┤
384
- │ dequal (passed) 756789.655029
417
+ │ dequal/lite (passed) 780805.627339
385
418
  ├────────────────────────────────────────┼────────────────┤
386
- fast-equals (circular) (passed) 726093.253185
419
+ dequal (passed) 767208.995048
387
420
  ├────────────────────────────────────────┼────────────────┤
388
- │ underscore.isEqual (passed) │ 489748.701783
421
+ │ underscore.isEqual (passed) │ 490695.830468
389
422
  ├────────────────────────────────────────┼────────────────┤
390
- │ assert.deepStrictEqual (passed) │ 453761.890107
423
+ │ assert.deepStrictEqual (passed) │ 471011.425391
391
424
  ├────────────────────────────────────────┼────────────────┤
392
- │ lodash.isEqual (passed) │ 288264.867811
425
+ │ lodash.isEqual (passed) │ 296064.057382
393
426
  ├────────────────────────────────────────┼────────────────┤
394
- │ fast-equals (strict) (passed) │ 217221.619705
427
+ │ fast-equals (strict) (passed) │ 225894.800964
395
428
  ├────────────────────────────────────────┼────────────────┤
396
- │ fast-equals (strict circular) (passed) │ 186916.942934
429
+ │ fast-equals (strict circular) (passed) │ 195657.732354
397
430
  ├────────────────────────────────────────┼────────────────┤
398
- │ deep-eql (passed) │ 162487.877883
431
+ │ deep-eql (passed) │ 162718.102328
399
432
  ├────────────────────────────────────────┼────────────────┤
400
- │ deep-equal (passed) │ 916.680714
433
+ │ deep-equal (passed) │ 954.172311
401
434
  └────────────────────────────────────────┴────────────────┘
402
435
 
403
436
  Testing mixed objects not equal...
404
437
  ┌────────────────────────────────────────┬────────────────┐
405
438
  │ Name │ Ops / sec │
406
439
  ├────────────────────────────────────────┼────────────────┤
407
- │ fast-equals (passed) │ 4687012.640614
440
+ │ fast-equals (passed) │ 5112341.000979
408
441
  ├────────────────────────────────────────┼────────────────┤
409
- │ fast-deep-equal (passed) 3418170.156109
442
+ │ fast-equals (circular) (passed) 3501225.300307
410
443
  ├────────────────────────────────────────┼────────────────┤
411
- react-fast-compare (passed) 3283516.669966
444
+ │ fast-deep-equal (passed) 3471838.735181
412
445
  ├────────────────────────────────────────┼────────────────┤
413
- │ fast-equals (circular) (passed) 3268062.099602
446
+ react-fast-compare (passed) 3439612.908273
414
447
  ├────────────────────────────────────────┼────────────────┤
415
- │ fast-equals (strict) (passed) │ 1747578.66456
448
+ │ fast-equals (strict) (passed) │ 1797319.423491
416
449
  ├────────────────────────────────────────┼────────────────┤
417
- │ fast-equals (strict circular) (passed) │ 1477873.624956
450
+ │ fast-equals (strict circular) (passed) │ 1534168.229167
418
451
  ├────────────────────────────────────────┼────────────────┤
419
- │ dequal/lite (passed) │ 1335397.839502
452
+ │ dequal/lite (passed) │ 1357981.758571
420
453
  ├────────────────────────────────────────┼────────────────┤
421
- │ dequal (passed) │ 1319426.71146
454
+ │ dequal (passed) │ 1328078.173967
422
455
  ├────────────────────────────────────────┼────────────────┤
423
- │ shallow-equal-fuzzy (failed) │ 1237432.986615
456
+ │ shallow-equal-fuzzy (failed) │ 1224747.272118
424
457
  ├────────────────────────────────────────┼────────────────┤
425
- │ nano-equal (passed) │ 1064383.319776
458
+ │ nano-equal (passed) │ 1087373.99615
426
459
  ├────────────────────────────────────────┼────────────────┤
427
- │ underscore.isEqual (passed) │ 920462.516736
460
+ │ underscore.isEqual (passed) │ 927298.592729
428
461
  ├────────────────────────────────────────┼────────────────┤
429
- │ lodash.isEqual (passed) │ 379370.998021
462
+ │ lodash.isEqual (passed) │ 387294.235476
430
463
  ├────────────────────────────────────────┼────────────────┤
431
- │ deep-eql (passed) │ 184111.383127
464
+ │ deep-eql (passed) │ 186028.168827
432
465
  ├────────────────────────────────────────┼────────────────┤
433
- │ assert.deepStrictEqual (passed) │ 20775.59065
466
+ │ assert.deepStrictEqual (passed) │ 21261.312424
434
467
  ├────────────────────────────────────────┼────────────────┤
435
- │ deep-equal (passed) │ 3678.51009
468
+ │ deep-equal (passed) │ 3782.329948
436
469
  └────────────────────────────────────────┴────────────────┘
437
470
  ```
438
471
 
@@ -442,17 +475,17 @@ Caveats that impact the benchmark (and accuracy of comparison):
442
475
  fully supported their comparison
443
476
  - `fast-deep-equal`, `react-fast-compare` and `nano-equal` throw on objects with `null` as prototype
444
477
  (`Object.create(null)`)
445
- - `assert.deepStrictEqual` does not support `NaN` or `SameValueZero` equality for dates
446
- - `deep-eql` does not support `SameValueZero` equality for zero equality (positive and negative zero are not equal)
478
+ - `assert.deepStrictEqual` does not support `NaN` or `SameValue` equality for dates
479
+ - `deep-eql` does not support `SameValue` equality for zero equality (positive and negative zero are not equal)
447
480
  - `deep-equal` does not support `NaN` and does not strictly compare object type, or date / regexp values, nor uses
448
- `SameValueZero` equality for dates
449
- - `fast-deep-equal` does not support `NaN` or `SameValueZero` equality for dates
450
- - `nano-equal` does not strictly compare object property structure, array length, or object type, nor `SameValueZero`
481
+ `SameValue` equality for dates
482
+ - `fast-deep-equal` does not support `NaN` or `SameValue` equality for dates
483
+ - `nano-equal` does not strictly compare object property structure, array length, or object type, nor `SameValue`
451
484
  equality for dates
452
- - `react-fast-compare` does not support `NaN` or `SameValueZero` equality for dates, and does not compare `function`
485
+ - `react-fast-compare` does not support `NaN` or `SameValue` equality for dates, and does not compare `function`
453
486
  equality
454
- - `shallow-equal-fuzzy` does not strictly compare object type or regexp values, nor `SameValueZero` equality for dates
455
- - `underscore.isEqual` does not support `SameValueZero` equality for primitives or dates
487
+ - `shallow-equal-fuzzy` does not strictly compare object type or regexp values, nor `SameValue` equality for dates
488
+ - `underscore.isEqual` does not support `SameValue` equality for primitives or dates
456
489
 
457
490
  All of these have the potential of inflating the respective library's numbers in comparison to `fast-equals`, but it was
458
491
  the closest apples-to-apples comparison I could create of a reasonable sample size. It should be noted that `react`
@@ -35,12 +35,6 @@ function createIsCircular(areItemsEqual) {
35
35
  return result;
36
36
  };
37
37
  }
38
- /**
39
- * Get the `@@toStringTag` of the value, if it exists.
40
- */
41
- function getShortTag(value) {
42
- return value != null ? value[Symbol.toStringTag] : undefined;
43
- }
44
38
  /**
45
39
  * Get the properties to strictly examine, which include both own properties that are
46
40
  * not enumerable and symbol properties.
@@ -54,17 +48,33 @@ function getStrictProperties(object) {
54
48
  const hasOwn =
55
49
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
56
50
  Object.hasOwn || ((object, property) => hasOwnProperty.call(object, property));
57
- /**
58
- * Whether the values passed are strictly equal or both NaN.
59
- */
60
- function sameValueZeroEqual(a, b) {
61
- return a === b || (!a && !b && a !== a && b !== b);
62
- }
63
51
 
64
52
  const PREACT_VNODE = '__v';
65
53
  const PREACT_OWNER = '__o';
66
54
  const REACT_OWNER = '_owner';
67
55
  const { getOwnPropertyDescriptor, keys } = Object;
56
+ /**
57
+ * Whether the values passed are equal based on a [SameValue](https://262.ecma-international.org/7.0/#sec-samevalue) basis.
58
+ * Simplified, this maps to if the two values are referentially equal to one another (`a === b`) or both are `NaN`.
59
+ *
60
+ * @note
61
+ * When available in the environment, this is just a re-export of the global
62
+ * [`Object.is`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is) method.
63
+ */
64
+ const sameValueEqual =
65
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
66
+ Object.is
67
+ || function sameValueEqual(a, b) {
68
+ return a === b ? a !== 0 || 1 / a === 1 / b : a !== a && b !== b;
69
+ };
70
+ /**
71
+ * Whether the values passed are equal based on a [SameValue](https://262.ecma-international.org/7.0/#sec-samevaluezero) basis.
72
+ * Simplified, this maps to if the two values are referentially equal to one another (`a === b`), both are `NaN`, or both
73
+ * are either positive or negative zero.
74
+ */
75
+ function sameValueZeroEqual(a, b) {
76
+ return a === b || (a !== a && b !== b);
77
+ }
68
78
  /**
69
79
  * Whether the array buffers are equal in value.
70
80
  */
@@ -97,7 +107,7 @@ function areDataViewsEqual(a, b) {
97
107
  * Whether the dates passed are equal in value.
98
108
  */
99
109
  function areDatesEqual(a, b) {
100
- return sameValueZeroEqual(a.getTime(), b.getTime());
110
+ return sameValueEqual(a.getTime(), b.getTime());
101
111
  }
102
112
  /**
103
113
  * Whether the errors passed are equal in value.
@@ -163,7 +173,7 @@ function areMapsEqual(a, b, state) {
163
173
  /**
164
174
  * Whether the numbers are equal in value.
165
175
  */
166
- const areNumbersEqual = sameValueZeroEqual;
176
+ const areNumbersEqual = sameValueEqual;
167
177
  /**
168
178
  * Whether the objects are equal in value.
169
179
  */
@@ -222,7 +232,7 @@ function areObjectsEqualStrict(a, b, state) {
222
232
  * Whether the primitive wrappers passed are equal in value.
223
233
  */
224
234
  function arePrimitiveWrappersEqual(a, b) {
225
- return sameValueZeroEqual(a.valueOf(), b.valueOf());
235
+ return sameValueEqual(a.valueOf(), b.valueOf());
226
236
  }
227
237
  /**
228
238
  * Whether the regexps passed are equal in value.
@@ -306,39 +316,14 @@ function isPropertyEqual(a, b, state, property) {
306
316
  return hasOwn(b, property) && state.equals(a[property], b[property], property, property, a, b, state);
307
317
  }
308
318
 
309
- const ARRAY_BUFFER_TAG = '[object ArrayBuffer]';
310
- const ARGUMENTS_TAG = '[object Arguments]';
311
- const BOOLEAN_TAG = '[object Boolean]';
312
- const DATA_VIEW_TAG = '[object DataView]';
313
- const DATE_TAG = '[object Date]';
314
- const ERROR_TAG = '[object Error]';
315
- const MAP_TAG = '[object Map]';
316
- const NUMBER_TAG = '[object Number]';
317
- const OBJECT_TAG = '[object Object]';
318
- const REG_EXP_TAG = '[object RegExp]';
319
- const SET_TAG = '[object Set]';
320
- const STRING_TAG = '[object String]';
321
- const TYPED_ARRAY_TAGS = {
322
- '[object Int8Array]': true,
323
- '[object Uint8Array]': true,
324
- '[object Uint8ClampedArray]': true,
325
- '[object Int16Array]': true,
326
- '[object Uint16Array]': true,
327
- '[object Int32Array]': true,
328
- '[object Uint32Array]': true,
329
- '[object Float16Array]': true,
330
- '[object Float32Array]': true,
331
- '[object Float64Array]': true,
332
- '[object BigInt64Array]': true,
333
- '[object BigUint64Array]': true,
334
- };
335
- const URL_TAG = '[object URL]';
336
319
  // eslint-disable-next-line @typescript-eslint/unbound-method
337
320
  const toString = Object.prototype.toString;
338
321
  /**
339
322
  * Create a comparator method based on the type-specific equality comparators passed.
340
323
  */
341
- function createEqualityComparator({ areArrayBuffersEqual, areArraysEqual, areDataViewsEqual, areDatesEqual, areErrorsEqual, areFunctionsEqual, areMapsEqual, areNumbersEqual, areObjectsEqual, arePrimitiveWrappersEqual, areRegExpsEqual, areSetsEqual, areTypedArraysEqual, areUrlsEqual, unknownTagComparators, }) {
324
+ function createEqualityComparator(config) {
325
+ const supportedComparatorMap = createSupportedComparatorMap(config);
326
+ const { areArraysEqual, areDatesEqual, areFunctionsEqual, areMapsEqual, areNumbersEqual, areObjectsEqual, areRegExpsEqual, areSetsEqual, getUnsupportedCustomComparator, } = config;
342
327
  /**
343
328
  * compare the value of the two objects and return true if they are equivalent in values
344
329
  */
@@ -357,7 +342,7 @@ function createEqualityComparator({ areArrayBuffersEqual, areArraysEqual, areDat
357
342
  return false;
358
343
  }
359
344
  if (type !== 'object') {
360
- if (type === 'number') {
345
+ if (type === 'number' || type === 'bigint') {
361
346
  return areNumbersEqual(a, b, state);
362
347
  }
363
348
  if (type === 'function') {
@@ -380,22 +365,17 @@ function createEqualityComparator({ areArrayBuffersEqual, areArraysEqual, areDat
380
365
  if (constructor !== b.constructor) {
381
366
  return false;
382
367
  }
383
- // `isPlainObject` only checks against the object's own realm. Cross-realm
384
- // comparisons are rare, and will be handled in the ultimate fallback, so
385
- // we can avoid capturing the string tag.
386
- if (constructor === Object) {
387
- return areObjectsEqual(a, b, state);
388
- }
389
- // `isArray()` works on subclasses and is cross-realm, so we can avoid capturing
390
- // the string tag or doing an `instanceof` check.
391
- if (Array.isArray(a)) {
392
- return areArraysEqual(a, b, state);
393
- }
394
368
  // Try to fast-path equality checks for other complex object types in the
395
369
  // same realm to avoid capturing the string tag. Strict equality is used
396
370
  // instead of `instanceof` because it is more performant for the common
397
371
  // use-case. If someone is subclassing a native class, it will be handled
398
372
  // with the string tag comparison.
373
+ if (constructor === Object) {
374
+ return areObjectsEqual(a, b, state);
375
+ }
376
+ if (constructor === Array) {
377
+ return areArraysEqual(a, b, state);
378
+ }
399
379
  if (constructor === Date) {
400
380
  return areDatesEqual(a, b, state);
401
381
  }
@@ -408,79 +388,31 @@ function createEqualityComparator({ areArrayBuffersEqual, areArraysEqual, areDat
408
388
  if (constructor === Set) {
409
389
  return areSetsEqual(a, b, state);
410
390
  }
391
+ if (constructor === Promise) {
392
+ // Avoid tag checks for promise values, since we know if they are not referentially equal
393
+ // then they are not equal.
394
+ return false;
395
+ }
396
+ // `isArray()` works on subclasses and is cross-realm, so we can avoid capturing
397
+ // the string tag or doing an `instanceof` in edge cases.
398
+ if (Array.isArray(a)) {
399
+ return areArraysEqual(a, b, state);
400
+ }
411
401
  // Since this is a custom object, capture the string tag to determing its type.
412
402
  // This is reasonably performant in modern environments like v8 and SpiderMonkey.
413
403
  const tag = toString.call(a);
414
- if (tag === DATE_TAG) {
415
- return areDatesEqual(a, b, state);
416
- }
417
- // For RegExp, the properties are not enumerable, and therefore will give false positives if
418
- // tested like a standard object.
419
- if (tag === REG_EXP_TAG) {
420
- return areRegExpsEqual(a, b, state);
421
- }
422
- if (tag === MAP_TAG) {
423
- return areMapsEqual(a, b, state);
424
- }
425
- if (tag === SET_TAG) {
426
- return areSetsEqual(a, b, state);
427
- }
428
- if (tag === OBJECT_TAG) {
429
- // The exception for value comparison is custom `Promise`-like class instances. These should
430
- // be treated the same as standard `Promise` objects, which means strict equality, and if
431
- // it reaches this point then that strict equality comparison has already failed.
432
- return typeof a.then !== 'function' && typeof b.then !== 'function' && areObjectsEqual(a, b, state);
433
- }
434
- // If a URL tag, it should be tested explicitly. Like RegExp, the properties are not
435
- // enumerable, and therefore will give false positives if tested like a standard object.
436
- if (tag === URL_TAG) {
437
- return areUrlsEqual(a, b, state);
438
- }
439
- // If an error tag, it should be tested explicitly. Like RegExp, the properties are not
440
- // enumerable, and therefore will give false positives if tested like a standard object.
441
- if (tag === ERROR_TAG) {
442
- return areErrorsEqual(a, b, state);
443
- }
444
- // If an arguments tag, it should be treated as a standard object.
445
- if (tag === ARGUMENTS_TAG) {
446
- return areObjectsEqual(a, b, state);
447
- }
448
- if (TYPED_ARRAY_TAGS[tag]) {
449
- return areTypedArraysEqual(a, b, state);
404
+ const supportedComparator = supportedComparatorMap[tag];
405
+ if (supportedComparator) {
406
+ return supportedComparator(a, b, state);
450
407
  }
451
- if (tag === ARRAY_BUFFER_TAG) {
452
- return areArrayBuffersEqual(a, b, state);
453
- }
454
- if (tag === DATA_VIEW_TAG) {
455
- return areDataViewsEqual(a, b, state);
456
- }
457
- // As the penultimate fallback, check if the values passed are primitive wrappers. This
458
- // is very rare in modern JS, which is why it is deprioritized compared to all other object
459
- // types.
460
- if (tag === BOOLEAN_TAG || tag === NUMBER_TAG || tag === STRING_TAG) {
461
- return arePrimitiveWrappersEqual(a, b, state);
462
- }
463
- if (unknownTagComparators) {
464
- let unknownTagComparator = unknownTagComparators[tag];
465
- if (!unknownTagComparator) {
466
- const shortTag = getShortTag(a);
467
- if (shortTag) {
468
- unknownTagComparator = unknownTagComparators[shortTag];
469
- }
470
- }
471
- // If the custom config has an unknown tag comparator that matches the captured tag or the
472
- // @@toStringTag, it is the source of truth for whether the values are equal.
473
- if (unknownTagComparator) {
474
- return unknownTagComparator(a, b, state);
475
- }
408
+ const unsupportedCustomComparator = getUnsupportedCustomComparator && getUnsupportedCustomComparator(a, b, tag);
409
+ if (unsupportedCustomComparator) {
410
+ return unsupportedCustomComparator(a, b, state);
476
411
  }
477
412
  // If not matching any tags that require a specific type of comparison, then we hard-code false because
478
413
  // the only thing remaining is strict equality, which has already been compared. This is for a few reasons:
479
414
  // - Certain types that cannot be introspected (e.g., `WeakMap`). For these types, this is the only
480
415
  // comparison that can be made.
481
- // - For types that can be introspected, but rarely have requirements to be compared
482
- // (`ArrayBuffer`, `DataView`, etc.), the cost is avoided to prioritize the common
483
- // use-cases (may be included in a future release, if requested enough).
484
416
  // - For types that can be introspected but do not have an objective definition of what
485
417
  // equality is (`Error`, etc.), the subjective decision is to be conservative and strictly compare.
486
418
  // In all cases, these decisions should be reevaluated based on changes to the language and
@@ -509,7 +441,7 @@ function createEqualityComparatorConfig({ circular, createCustomConfig, strict,
509
441
  ? combineComparators(areTypedArraysEqual, areObjectsEqualStrict)
510
442
  : areTypedArraysEqual,
511
443
  areUrlsEqual: areUrlsEqual,
512
- unknownTagComparators: undefined,
444
+ getUnsupportedCustomComparator: undefined,
513
445
  };
514
446
  if (createCustomConfig) {
515
447
  config = Object.assign({}, config, createCustomConfig(config));
@@ -572,6 +504,50 @@ function createIsEqual({ circular, comparator, createState, equals, strict }) {
572
504
  return comparator(a, b, state);
573
505
  };
574
506
  }
507
+ /**
508
+ * Create a map of `toString()` values to their respective handlers for `tag`-based lookups.
509
+ */
510
+ function createSupportedComparatorMap({ areArrayBuffersEqual, areArraysEqual, areDataViewsEqual, areDatesEqual, areErrorsEqual, areFunctionsEqual, areMapsEqual, areNumbersEqual, areObjectsEqual, arePrimitiveWrappersEqual, areRegExpsEqual, areSetsEqual, areTypedArraysEqual, areUrlsEqual, }) {
511
+ return {
512
+ '[object Arguments]': areObjectsEqual,
513
+ '[object Array]': areArraysEqual,
514
+ '[object ArrayBuffer]': areArrayBuffersEqual,
515
+ '[object BigInt]': areNumbersEqual,
516
+ '[object BigInt64Array]': areTypedArraysEqual,
517
+ '[object BigUint64Array]': areTypedArraysEqual,
518
+ '[object Boolean]': arePrimitiveWrappersEqual,
519
+ '[object DataView]': areDataViewsEqual,
520
+ '[object Date]': areDatesEqual,
521
+ // If an error tag, it should be tested explicitly. Like RegExp, the properties are not
522
+ // enumerable, and therefore will give false positives if tested like a standard object.
523
+ '[object Error]': areErrorsEqual,
524
+ '[object Float16Array]': areTypedArraysEqual,
525
+ '[object Float32Array]': areTypedArraysEqual,
526
+ '[object Float64Array]': areTypedArraysEqual,
527
+ '[object Function]': areFunctionsEqual,
528
+ '[object GeneratorFunction]': areFunctionsEqual,
529
+ '[object Int8Array]': areTypedArraysEqual,
530
+ '[object Int16Array]': areTypedArraysEqual,
531
+ '[object Int32Array]': areTypedArraysEqual,
532
+ '[object Map]': areMapsEqual,
533
+ '[object Number]': arePrimitiveWrappersEqual,
534
+ '[object Object]': (a, b, state) =>
535
+ // The exception for value comparison is custom `Promise`-like class instances. These should
536
+ // be treated the same as standard `Promise` objects, which means strict equality, and if
537
+ // it reaches this point then that strict equality comparison has already failed.
538
+ typeof a.then !== 'function' && typeof b.then !== 'function' && areObjectsEqual(a, b, state),
539
+ // For RegExp, the properties are not enumerable, and therefore will give false positives if
540
+ // tested like a standard object.
541
+ '[object RegExp]': areRegExpsEqual,
542
+ '[object Set]': areSetsEqual,
543
+ '[object String]': arePrimitiveWrappersEqual,
544
+ '[object URL]': areUrlsEqual,
545
+ '[object Uint8Array]': areTypedArraysEqual,
546
+ '[object Uint8ClampedArray]': areTypedArraysEqual,
547
+ '[object Uint16Array]': areTypedArraysEqual,
548
+ '[object Uint32Array]': areTypedArraysEqual,
549
+ };
550
+ }
575
551
 
576
552
  /**
577
553
  * Whether the items passed are deeply-equal in value.
@@ -597,21 +573,21 @@ const strictCircularDeepEqual = createCustomEqual({
597
573
  * Whether the items passed are shallowly-equal in value.
598
574
  */
599
575
  const shallowEqual = createCustomEqual({
600
- createInternalComparator: () => sameValueZeroEqual,
576
+ createInternalComparator: () => sameValueEqual,
601
577
  });
602
578
  /**
603
579
  * Whether the items passed are shallowly-equal in value based on strict comparison
604
580
  */
605
581
  const strictShallowEqual = createCustomEqual({
606
582
  strict: true,
607
- createInternalComparator: () => sameValueZeroEqual,
583
+ createInternalComparator: () => sameValueEqual,
608
584
  });
609
585
  /**
610
586
  * Whether the items passed are shallowly-equal in value, including circular references.
611
587
  */
612
588
  const circularShallowEqual = createCustomEqual({
613
589
  circular: true,
614
- createInternalComparator: () => sameValueZeroEqual,
590
+ createInternalComparator: () => sameValueEqual,
615
591
  });
616
592
  /**
617
593
  * Whether the items passed are shallowly-equal in value, including circular references,
@@ -619,7 +595,7 @@ const circularShallowEqual = createCustomEqual({
619
595
  */
620
596
  const strictCircularShallowEqual = createCustomEqual({
621
597
  circular: true,
622
- createInternalComparator: () => sameValueZeroEqual,
598
+ createInternalComparator: () => sameValueEqual,
623
599
  strict: true,
624
600
  });
625
601
  /**
@@ -644,6 +620,7 @@ exports.circularDeepEqual = circularDeepEqual;
644
620
  exports.circularShallowEqual = circularShallowEqual;
645
621
  exports.createCustomEqual = createCustomEqual;
646
622
  exports.deepEqual = deepEqual;
623
+ exports.sameValueEqual = sameValueEqual;
647
624
  exports.sameValueZeroEqual = sameValueZeroEqual;
648
625
  exports.shallowEqual = shallowEqual;
649
626
  exports.strictCircularDeepEqual = strictCircularDeepEqual;
@@ -101,12 +101,9 @@ interface ComparatorConfig<Meta> {
101
101
  */
102
102
  areUrlsEqual: TypeEqualityComparator<any, Meta>;
103
103
  /**
104
- * Whether two values with unknown `@@toStringTag` are equal in value. This comparator is
105
- * called when no other comparator applies.
106
- *
107
- * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/toStringTag
104
+ * Get a custom comparator based on the objects passed.
108
105
  */
109
- unknownTagComparators: Record<string, TypeEqualityComparator<any, Meta>> | undefined;
106
+ getUnsupportedCustomComparator: ((a: any, b: any, tag: string) => TypeEqualityComparator<any, Meta> | undefined) | undefined;
110
107
  }
111
108
  type CreateCustomComparatorConfig<Meta> = (config: ComparatorConfig<Meta>) => Partial<ComparatorConfig<Meta>>;
112
109
  type CreateState<Meta> = () => {
@@ -159,7 +156,18 @@ interface CustomEqualCreatorOptions<Meta> {
159
156
  }
160
157
 
161
158
  /**
162
- * Whether the values passed are strictly equal or both NaN.
159
+ * Whether the values passed are equal based on a [SameValue](https://262.ecma-international.org/7.0/#sec-samevalue) basis.
160
+ * Simplified, this maps to if the two values are referentially equal to one another (`a === b`) or both are `NaN`.
161
+ *
162
+ * @note
163
+ * When available in the environment, this is just a re-export of the global
164
+ * [`Object.is`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is) method.
165
+ */
166
+ declare const sameValueEqual: (value1: any, value2: any) => boolean;
167
+ /**
168
+ * Whether the values passed are equal based on a [SameValue](https://262.ecma-international.org/7.0/#sec-samevaluezero) basis.
169
+ * Simplified, this maps to if the two values are referentially equal to one another (`a === b`), both are `NaN`, or both
170
+ * are either positive or negative zero.
163
171
  */
164
172
  declare function sameValueZeroEqual(a: any, b: any): boolean;
165
173
 
@@ -207,5 +215,5 @@ declare const strictCircularShallowEqual: <A, B>(a: A, b: B) => boolean;
207
215
  */
208
216
  declare function createCustomEqual<Meta = undefined>(options?: CustomEqualCreatorOptions<Meta>): <A, B>(a: A, b: B) => boolean;
209
217
 
210
- export { circularDeepEqual, circularShallowEqual, createCustomEqual, deepEqual, sameValueZeroEqual, shallowEqual, strictCircularDeepEqual, strictCircularShallowEqual, strictDeepEqual, strictShallowEqual };
218
+ export { circularDeepEqual, circularShallowEqual, createCustomEqual, deepEqual, sameValueEqual, sameValueZeroEqual, shallowEqual, strictCircularDeepEqual, strictCircularShallowEqual, strictDeepEqual, strictShallowEqual };
211
219
  export type { AnyEqualityComparator, Cache, CircularState, ComparatorConfig, CreateCustomComparatorConfig, CreateState, CustomEqualCreatorOptions, DefaultState, Dictionary, EqualityComparator, EqualityComparatorCreator, InternalEqualityComparator, PrimitiveWrapper, State, TypeEqualityComparator, TypedArray };
@@ -101,12 +101,9 @@ interface ComparatorConfig<Meta> {
101
101
  */
102
102
  areUrlsEqual: TypeEqualityComparator<any, Meta>;
103
103
  /**
104
- * Whether two values with unknown `@@toStringTag` are equal in value. This comparator is
105
- * called when no other comparator applies.
106
- *
107
- * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/toStringTag
104
+ * Get a custom comparator based on the objects passed.
108
105
  */
109
- unknownTagComparators: Record<string, TypeEqualityComparator<any, Meta>> | undefined;
106
+ getUnsupportedCustomComparator: ((a: any, b: any, tag: string) => TypeEqualityComparator<any, Meta> | undefined) | undefined;
110
107
  }
111
108
  type CreateCustomComparatorConfig<Meta> = (config: ComparatorConfig<Meta>) => Partial<ComparatorConfig<Meta>>;
112
109
  type CreateState<Meta> = () => {
@@ -159,7 +156,18 @@ interface CustomEqualCreatorOptions<Meta> {
159
156
  }
160
157
 
161
158
  /**
162
- * Whether the values passed are strictly equal or both NaN.
159
+ * Whether the values passed are equal based on a [SameValue](https://262.ecma-international.org/7.0/#sec-samevalue) basis.
160
+ * Simplified, this maps to if the two values are referentially equal to one another (`a === b`) or both are `NaN`.
161
+ *
162
+ * @note
163
+ * When available in the environment, this is just a re-export of the global
164
+ * [`Object.is`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is) method.
165
+ */
166
+ declare const sameValueEqual: (value1: any, value2: any) => boolean;
167
+ /**
168
+ * Whether the values passed are equal based on a [SameValue](https://262.ecma-international.org/7.0/#sec-samevaluezero) basis.
169
+ * Simplified, this maps to if the two values are referentially equal to one another (`a === b`), both are `NaN`, or both
170
+ * are either positive or negative zero.
163
171
  */
164
172
  declare function sameValueZeroEqual(a: any, b: any): boolean;
165
173
 
@@ -207,5 +215,5 @@ declare const strictCircularShallowEqual: <A, B>(a: A, b: B) => boolean;
207
215
  */
208
216
  declare function createCustomEqual<Meta = undefined>(options?: CustomEqualCreatorOptions<Meta>): <A, B>(a: A, b: B) => boolean;
209
217
 
210
- export { circularDeepEqual, circularShallowEqual, createCustomEqual, deepEqual, sameValueZeroEqual, shallowEqual, strictCircularDeepEqual, strictCircularShallowEqual, strictDeepEqual, strictShallowEqual };
218
+ export { circularDeepEqual, circularShallowEqual, createCustomEqual, deepEqual, sameValueEqual, sameValueZeroEqual, shallowEqual, strictCircularDeepEqual, strictCircularShallowEqual, strictDeepEqual, strictShallowEqual };
211
219
  export type { AnyEqualityComparator, Cache, CircularState, ComparatorConfig, CreateCustomComparatorConfig, CreateState, CustomEqualCreatorOptions, DefaultState, Dictionary, EqualityComparator, EqualityComparatorCreator, InternalEqualityComparator, PrimitiveWrapper, State, TypeEqualityComparator, TypedArray };
package/dist/es/index.mjs CHANGED
@@ -33,12 +33,6 @@ function createIsCircular(areItemsEqual) {
33
33
  return result;
34
34
  };
35
35
  }
36
- /**
37
- * Get the `@@toStringTag` of the value, if it exists.
38
- */
39
- function getShortTag(value) {
40
- return value != null ? value[Symbol.toStringTag] : undefined;
41
- }
42
36
  /**
43
37
  * Get the properties to strictly examine, which include both own properties that are
44
38
  * not enumerable and symbol properties.
@@ -52,17 +46,33 @@ function getStrictProperties(object) {
52
46
  const hasOwn =
53
47
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
54
48
  Object.hasOwn || ((object, property) => hasOwnProperty.call(object, property));
55
- /**
56
- * Whether the values passed are strictly equal or both NaN.
57
- */
58
- function sameValueZeroEqual(a, b) {
59
- return a === b || (!a && !b && a !== a && b !== b);
60
- }
61
49
 
62
50
  const PREACT_VNODE = '__v';
63
51
  const PREACT_OWNER = '__o';
64
52
  const REACT_OWNER = '_owner';
65
53
  const { getOwnPropertyDescriptor, keys } = Object;
54
+ /**
55
+ * Whether the values passed are equal based on a [SameValue](https://262.ecma-international.org/7.0/#sec-samevalue) basis.
56
+ * Simplified, this maps to if the two values are referentially equal to one another (`a === b`) or both are `NaN`.
57
+ *
58
+ * @note
59
+ * When available in the environment, this is just a re-export of the global
60
+ * [`Object.is`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is) method.
61
+ */
62
+ const sameValueEqual =
63
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
64
+ Object.is
65
+ || function sameValueEqual(a, b) {
66
+ return a === b ? a !== 0 || 1 / a === 1 / b : a !== a && b !== b;
67
+ };
68
+ /**
69
+ * Whether the values passed are equal based on a [SameValue](https://262.ecma-international.org/7.0/#sec-samevaluezero) basis.
70
+ * Simplified, this maps to if the two values are referentially equal to one another (`a === b`), both are `NaN`, or both
71
+ * are either positive or negative zero.
72
+ */
73
+ function sameValueZeroEqual(a, b) {
74
+ return a === b || (a !== a && b !== b);
75
+ }
66
76
  /**
67
77
  * Whether the array buffers are equal in value.
68
78
  */
@@ -95,7 +105,7 @@ function areDataViewsEqual(a, b) {
95
105
  * Whether the dates passed are equal in value.
96
106
  */
97
107
  function areDatesEqual(a, b) {
98
- return sameValueZeroEqual(a.getTime(), b.getTime());
108
+ return sameValueEqual(a.getTime(), b.getTime());
99
109
  }
100
110
  /**
101
111
  * Whether the errors passed are equal in value.
@@ -161,7 +171,7 @@ function areMapsEqual(a, b, state) {
161
171
  /**
162
172
  * Whether the numbers are equal in value.
163
173
  */
164
- const areNumbersEqual = sameValueZeroEqual;
174
+ const areNumbersEqual = sameValueEqual;
165
175
  /**
166
176
  * Whether the objects are equal in value.
167
177
  */
@@ -220,7 +230,7 @@ function areObjectsEqualStrict(a, b, state) {
220
230
  * Whether the primitive wrappers passed are equal in value.
221
231
  */
222
232
  function arePrimitiveWrappersEqual(a, b) {
223
- return sameValueZeroEqual(a.valueOf(), b.valueOf());
233
+ return sameValueEqual(a.valueOf(), b.valueOf());
224
234
  }
225
235
  /**
226
236
  * Whether the regexps passed are equal in value.
@@ -304,39 +314,14 @@ function isPropertyEqual(a, b, state, property) {
304
314
  return hasOwn(b, property) && state.equals(a[property], b[property], property, property, a, b, state);
305
315
  }
306
316
 
307
- const ARRAY_BUFFER_TAG = '[object ArrayBuffer]';
308
- const ARGUMENTS_TAG = '[object Arguments]';
309
- const BOOLEAN_TAG = '[object Boolean]';
310
- const DATA_VIEW_TAG = '[object DataView]';
311
- const DATE_TAG = '[object Date]';
312
- const ERROR_TAG = '[object Error]';
313
- const MAP_TAG = '[object Map]';
314
- const NUMBER_TAG = '[object Number]';
315
- const OBJECT_TAG = '[object Object]';
316
- const REG_EXP_TAG = '[object RegExp]';
317
- const SET_TAG = '[object Set]';
318
- const STRING_TAG = '[object String]';
319
- const TYPED_ARRAY_TAGS = {
320
- '[object Int8Array]': true,
321
- '[object Uint8Array]': true,
322
- '[object Uint8ClampedArray]': true,
323
- '[object Int16Array]': true,
324
- '[object Uint16Array]': true,
325
- '[object Int32Array]': true,
326
- '[object Uint32Array]': true,
327
- '[object Float16Array]': true,
328
- '[object Float32Array]': true,
329
- '[object Float64Array]': true,
330
- '[object BigInt64Array]': true,
331
- '[object BigUint64Array]': true,
332
- };
333
- const URL_TAG = '[object URL]';
334
317
  // eslint-disable-next-line @typescript-eslint/unbound-method
335
318
  const toString = Object.prototype.toString;
336
319
  /**
337
320
  * Create a comparator method based on the type-specific equality comparators passed.
338
321
  */
339
- function createEqualityComparator({ areArrayBuffersEqual, areArraysEqual, areDataViewsEqual, areDatesEqual, areErrorsEqual, areFunctionsEqual, areMapsEqual, areNumbersEqual, areObjectsEqual, arePrimitiveWrappersEqual, areRegExpsEqual, areSetsEqual, areTypedArraysEqual, areUrlsEqual, unknownTagComparators, }) {
322
+ function createEqualityComparator(config) {
323
+ const supportedComparatorMap = createSupportedComparatorMap(config);
324
+ const { areArraysEqual, areDatesEqual, areFunctionsEqual, areMapsEqual, areNumbersEqual, areObjectsEqual, areRegExpsEqual, areSetsEqual, getUnsupportedCustomComparator, } = config;
340
325
  /**
341
326
  * compare the value of the two objects and return true if they are equivalent in values
342
327
  */
@@ -355,7 +340,7 @@ function createEqualityComparator({ areArrayBuffersEqual, areArraysEqual, areDat
355
340
  return false;
356
341
  }
357
342
  if (type !== 'object') {
358
- if (type === 'number') {
343
+ if (type === 'number' || type === 'bigint') {
359
344
  return areNumbersEqual(a, b, state);
360
345
  }
361
346
  if (type === 'function') {
@@ -378,22 +363,17 @@ function createEqualityComparator({ areArrayBuffersEqual, areArraysEqual, areDat
378
363
  if (constructor !== b.constructor) {
379
364
  return false;
380
365
  }
381
- // `isPlainObject` only checks against the object's own realm. Cross-realm
382
- // comparisons are rare, and will be handled in the ultimate fallback, so
383
- // we can avoid capturing the string tag.
384
- if (constructor === Object) {
385
- return areObjectsEqual(a, b, state);
386
- }
387
- // `isArray()` works on subclasses and is cross-realm, so we can avoid capturing
388
- // the string tag or doing an `instanceof` check.
389
- if (Array.isArray(a)) {
390
- return areArraysEqual(a, b, state);
391
- }
392
366
  // Try to fast-path equality checks for other complex object types in the
393
367
  // same realm to avoid capturing the string tag. Strict equality is used
394
368
  // instead of `instanceof` because it is more performant for the common
395
369
  // use-case. If someone is subclassing a native class, it will be handled
396
370
  // with the string tag comparison.
371
+ if (constructor === Object) {
372
+ return areObjectsEqual(a, b, state);
373
+ }
374
+ if (constructor === Array) {
375
+ return areArraysEqual(a, b, state);
376
+ }
397
377
  if (constructor === Date) {
398
378
  return areDatesEqual(a, b, state);
399
379
  }
@@ -406,79 +386,31 @@ function createEqualityComparator({ areArrayBuffersEqual, areArraysEqual, areDat
406
386
  if (constructor === Set) {
407
387
  return areSetsEqual(a, b, state);
408
388
  }
389
+ if (constructor === Promise) {
390
+ // Avoid tag checks for promise values, since we know if they are not referentially equal
391
+ // then they are not equal.
392
+ return false;
393
+ }
394
+ // `isArray()` works on subclasses and is cross-realm, so we can avoid capturing
395
+ // the string tag or doing an `instanceof` in edge cases.
396
+ if (Array.isArray(a)) {
397
+ return areArraysEqual(a, b, state);
398
+ }
409
399
  // Since this is a custom object, capture the string tag to determing its type.
410
400
  // This is reasonably performant in modern environments like v8 and SpiderMonkey.
411
401
  const tag = toString.call(a);
412
- if (tag === DATE_TAG) {
413
- return areDatesEqual(a, b, state);
414
- }
415
- // For RegExp, the properties are not enumerable, and therefore will give false positives if
416
- // tested like a standard object.
417
- if (tag === REG_EXP_TAG) {
418
- return areRegExpsEqual(a, b, state);
419
- }
420
- if (tag === MAP_TAG) {
421
- return areMapsEqual(a, b, state);
422
- }
423
- if (tag === SET_TAG) {
424
- return areSetsEqual(a, b, state);
425
- }
426
- if (tag === OBJECT_TAG) {
427
- // The exception for value comparison is custom `Promise`-like class instances. These should
428
- // be treated the same as standard `Promise` objects, which means strict equality, and if
429
- // it reaches this point then that strict equality comparison has already failed.
430
- return typeof a.then !== 'function' && typeof b.then !== 'function' && areObjectsEqual(a, b, state);
431
- }
432
- // If a URL tag, it should be tested explicitly. Like RegExp, the properties are not
433
- // enumerable, and therefore will give false positives if tested like a standard object.
434
- if (tag === URL_TAG) {
435
- return areUrlsEqual(a, b, state);
436
- }
437
- // If an error tag, it should be tested explicitly. Like RegExp, the properties are not
438
- // enumerable, and therefore will give false positives if tested like a standard object.
439
- if (tag === ERROR_TAG) {
440
- return areErrorsEqual(a, b, state);
441
- }
442
- // If an arguments tag, it should be treated as a standard object.
443
- if (tag === ARGUMENTS_TAG) {
444
- return areObjectsEqual(a, b, state);
445
- }
446
- if (TYPED_ARRAY_TAGS[tag]) {
447
- return areTypedArraysEqual(a, b, state);
402
+ const supportedComparator = supportedComparatorMap[tag];
403
+ if (supportedComparator) {
404
+ return supportedComparator(a, b, state);
448
405
  }
449
- if (tag === ARRAY_BUFFER_TAG) {
450
- return areArrayBuffersEqual(a, b, state);
451
- }
452
- if (tag === DATA_VIEW_TAG) {
453
- return areDataViewsEqual(a, b, state);
454
- }
455
- // As the penultimate fallback, check if the values passed are primitive wrappers. This
456
- // is very rare in modern JS, which is why it is deprioritized compared to all other object
457
- // types.
458
- if (tag === BOOLEAN_TAG || tag === NUMBER_TAG || tag === STRING_TAG) {
459
- return arePrimitiveWrappersEqual(a, b, state);
460
- }
461
- if (unknownTagComparators) {
462
- let unknownTagComparator = unknownTagComparators[tag];
463
- if (!unknownTagComparator) {
464
- const shortTag = getShortTag(a);
465
- if (shortTag) {
466
- unknownTagComparator = unknownTagComparators[shortTag];
467
- }
468
- }
469
- // If the custom config has an unknown tag comparator that matches the captured tag or the
470
- // @@toStringTag, it is the source of truth for whether the values are equal.
471
- if (unknownTagComparator) {
472
- return unknownTagComparator(a, b, state);
473
- }
406
+ const unsupportedCustomComparator = getUnsupportedCustomComparator && getUnsupportedCustomComparator(a, b, tag);
407
+ if (unsupportedCustomComparator) {
408
+ return unsupportedCustomComparator(a, b, state);
474
409
  }
475
410
  // If not matching any tags that require a specific type of comparison, then we hard-code false because
476
411
  // the only thing remaining is strict equality, which has already been compared. This is for a few reasons:
477
412
  // - Certain types that cannot be introspected (e.g., `WeakMap`). For these types, this is the only
478
413
  // comparison that can be made.
479
- // - For types that can be introspected, but rarely have requirements to be compared
480
- // (`ArrayBuffer`, `DataView`, etc.), the cost is avoided to prioritize the common
481
- // use-cases (may be included in a future release, if requested enough).
482
414
  // - For types that can be introspected but do not have an objective definition of what
483
415
  // equality is (`Error`, etc.), the subjective decision is to be conservative and strictly compare.
484
416
  // In all cases, these decisions should be reevaluated based on changes to the language and
@@ -507,7 +439,7 @@ function createEqualityComparatorConfig({ circular, createCustomConfig, strict,
507
439
  ? combineComparators(areTypedArraysEqual, areObjectsEqualStrict)
508
440
  : areTypedArraysEqual,
509
441
  areUrlsEqual: areUrlsEqual,
510
- unknownTagComparators: undefined,
442
+ getUnsupportedCustomComparator: undefined,
511
443
  };
512
444
  if (createCustomConfig) {
513
445
  config = Object.assign({}, config, createCustomConfig(config));
@@ -570,6 +502,50 @@ function createIsEqual({ circular, comparator, createState, equals, strict }) {
570
502
  return comparator(a, b, state);
571
503
  };
572
504
  }
505
+ /**
506
+ * Create a map of `toString()` values to their respective handlers for `tag`-based lookups.
507
+ */
508
+ function createSupportedComparatorMap({ areArrayBuffersEqual, areArraysEqual, areDataViewsEqual, areDatesEqual, areErrorsEqual, areFunctionsEqual, areMapsEqual, areNumbersEqual, areObjectsEqual, arePrimitiveWrappersEqual, areRegExpsEqual, areSetsEqual, areTypedArraysEqual, areUrlsEqual, }) {
509
+ return {
510
+ '[object Arguments]': areObjectsEqual,
511
+ '[object Array]': areArraysEqual,
512
+ '[object ArrayBuffer]': areArrayBuffersEqual,
513
+ '[object BigInt]': areNumbersEqual,
514
+ '[object BigInt64Array]': areTypedArraysEqual,
515
+ '[object BigUint64Array]': areTypedArraysEqual,
516
+ '[object Boolean]': arePrimitiveWrappersEqual,
517
+ '[object DataView]': areDataViewsEqual,
518
+ '[object Date]': areDatesEqual,
519
+ // If an error tag, it should be tested explicitly. Like RegExp, the properties are not
520
+ // enumerable, and therefore will give false positives if tested like a standard object.
521
+ '[object Error]': areErrorsEqual,
522
+ '[object Float16Array]': areTypedArraysEqual,
523
+ '[object Float32Array]': areTypedArraysEqual,
524
+ '[object Float64Array]': areTypedArraysEqual,
525
+ '[object Function]': areFunctionsEqual,
526
+ '[object GeneratorFunction]': areFunctionsEqual,
527
+ '[object Int8Array]': areTypedArraysEqual,
528
+ '[object Int16Array]': areTypedArraysEqual,
529
+ '[object Int32Array]': areTypedArraysEqual,
530
+ '[object Map]': areMapsEqual,
531
+ '[object Number]': arePrimitiveWrappersEqual,
532
+ '[object Object]': (a, b, state) =>
533
+ // The exception for value comparison is custom `Promise`-like class instances. These should
534
+ // be treated the same as standard `Promise` objects, which means strict equality, and if
535
+ // it reaches this point then that strict equality comparison has already failed.
536
+ typeof a.then !== 'function' && typeof b.then !== 'function' && areObjectsEqual(a, b, state),
537
+ // For RegExp, the properties are not enumerable, and therefore will give false positives if
538
+ // tested like a standard object.
539
+ '[object RegExp]': areRegExpsEqual,
540
+ '[object Set]': areSetsEqual,
541
+ '[object String]': arePrimitiveWrappersEqual,
542
+ '[object URL]': areUrlsEqual,
543
+ '[object Uint8Array]': areTypedArraysEqual,
544
+ '[object Uint8ClampedArray]': areTypedArraysEqual,
545
+ '[object Uint16Array]': areTypedArraysEqual,
546
+ '[object Uint32Array]': areTypedArraysEqual,
547
+ };
548
+ }
573
549
 
574
550
  /**
575
551
  * Whether the items passed are deeply-equal in value.
@@ -595,21 +571,21 @@ const strictCircularDeepEqual = createCustomEqual({
595
571
  * Whether the items passed are shallowly-equal in value.
596
572
  */
597
573
  const shallowEqual = createCustomEqual({
598
- createInternalComparator: () => sameValueZeroEqual,
574
+ createInternalComparator: () => sameValueEqual,
599
575
  });
600
576
  /**
601
577
  * Whether the items passed are shallowly-equal in value based on strict comparison
602
578
  */
603
579
  const strictShallowEqual = createCustomEqual({
604
580
  strict: true,
605
- createInternalComparator: () => sameValueZeroEqual,
581
+ createInternalComparator: () => sameValueEqual,
606
582
  });
607
583
  /**
608
584
  * Whether the items passed are shallowly-equal in value, including circular references.
609
585
  */
610
586
  const circularShallowEqual = createCustomEqual({
611
587
  circular: true,
612
- createInternalComparator: () => sameValueZeroEqual,
588
+ createInternalComparator: () => sameValueEqual,
613
589
  });
614
590
  /**
615
591
  * Whether the items passed are shallowly-equal in value, including circular references,
@@ -617,7 +593,7 @@ const circularShallowEqual = createCustomEqual({
617
593
  */
618
594
  const strictCircularShallowEqual = createCustomEqual({
619
595
  circular: true,
620
- createInternalComparator: () => sameValueZeroEqual,
596
+ createInternalComparator: () => sameValueEqual,
621
597
  strict: true,
622
598
  });
623
599
  /**
@@ -638,4 +614,4 @@ function createCustomEqual(options = {}) {
638
614
  return createIsEqual({ circular, comparator, createState, equals, strict });
639
615
  }
640
616
 
641
- export { circularDeepEqual, circularShallowEqual, createCustomEqual, deepEqual, sameValueZeroEqual, shallowEqual, strictCircularDeepEqual, strictCircularShallowEqual, strictDeepEqual, strictShallowEqual };
617
+ export { circularDeepEqual, circularShallowEqual, createCustomEqual, deepEqual, sameValueEqual, sameValueZeroEqual, shallowEqual, strictCircularDeepEqual, strictCircularShallowEqual, strictDeepEqual, strictShallowEqual };
package/index.d.ts CHANGED
@@ -101,12 +101,9 @@ interface ComparatorConfig<Meta> {
101
101
  */
102
102
  areUrlsEqual: TypeEqualityComparator<any, Meta>;
103
103
  /**
104
- * Whether two values with unknown `@@toStringTag` are equal in value. This comparator is
105
- * called when no other comparator applies.
106
- *
107
- * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/toStringTag
104
+ * Get a custom comparator based on the objects passed.
108
105
  */
109
- unknownTagComparators: Record<string, TypeEqualityComparator<any, Meta>> | undefined;
106
+ getUnsupportedCustomComparator: ((a: any, b: any, tag: string) => TypeEqualityComparator<any, Meta> | undefined) | undefined;
110
107
  }
111
108
  type CreateCustomComparatorConfig<Meta> = (config: ComparatorConfig<Meta>) => Partial<ComparatorConfig<Meta>>;
112
109
  type CreateState<Meta> = () => {
@@ -159,7 +156,18 @@ interface CustomEqualCreatorOptions<Meta> {
159
156
  }
160
157
 
161
158
  /**
162
- * Whether the values passed are strictly equal or both NaN.
159
+ * Whether the values passed are equal based on a [SameValue](https://262.ecma-international.org/7.0/#sec-samevalue) basis.
160
+ * Simplified, this maps to if the two values are referentially equal to one another (`a === b`) or both are `NaN`.
161
+ *
162
+ * @note
163
+ * When available in the environment, this is just a re-export of the global
164
+ * [`Object.is`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is) method.
165
+ */
166
+ declare const sameValueEqual: (value1: any, value2: any) => boolean;
167
+ /**
168
+ * Whether the values passed are equal based on a [SameValue](https://262.ecma-international.org/7.0/#sec-samevaluezero) basis.
169
+ * Simplified, this maps to if the two values are referentially equal to one another (`a === b`), both are `NaN`, or both
170
+ * are either positive or negative zero.
163
171
  */
164
172
  declare function sameValueZeroEqual(a: any, b: any): boolean;
165
173
 
@@ -207,5 +215,5 @@ declare const strictCircularShallowEqual: <A, B>(a: A, b: B) => boolean;
207
215
  */
208
216
  declare function createCustomEqual<Meta = undefined>(options?: CustomEqualCreatorOptions<Meta>): <A, B>(a: A, b: B) => boolean;
209
217
 
210
- export { circularDeepEqual, circularShallowEqual, createCustomEqual, deepEqual, sameValueZeroEqual, shallowEqual, strictCircularDeepEqual, strictCircularShallowEqual, strictDeepEqual, strictShallowEqual };
218
+ export { circularDeepEqual, circularShallowEqual, createCustomEqual, deepEqual, sameValueEqual, sameValueZeroEqual, shallowEqual, strictCircularDeepEqual, strictCircularShallowEqual, strictDeepEqual, strictShallowEqual };
211
219
  export type { AnyEqualityComparator, Cache, CircularState, ComparatorConfig, CreateCustomComparatorConfig, CreateState, CustomEqualCreatorOptions, DefaultState, Dictionary, EqualityComparator, EqualityComparatorCreator, InternalEqualityComparator, PrimitiveWrapper, State, TypeEqualityComparator, TypedArray };
package/package.json CHANGED
@@ -9,14 +9,14 @@
9
9
  },
10
10
  "description": "A blazing fast equality comparison, either shallow or deep",
11
11
  "devDependencies": {
12
- "@planttheidea/build-tools": "^2.0.0-beta.5",
12
+ "@planttheidea/build-tools": "^2.0.0",
13
13
  "@types/lodash": "^4.17.21",
14
- "@types/node": "^24.10.3",
14
+ "@types/node": "^24.10.4",
15
15
  "@types/ramda": "^0.31.1",
16
16
  "@types/react": "^19.2.7",
17
17
  "@types/react-dom": "^19.2.3",
18
- "@typescript-eslint/eslint-plugin": "^8.49.0",
19
- "@typescript-eslint/parser": "^8.49.0",
18
+ "@typescript-eslint/eslint-plugin": "^8.50.0",
19
+ "@typescript-eslint/parser": "^8.50.0",
20
20
  "@vitest/coverage-v8": "^4.0.15",
21
21
  "cli-table3": "^0.6.5",
22
22
  "decircularize": "^1.0.0",
@@ -35,13 +35,13 @@
35
35
  "react-dom": "^19.2.3",
36
36
  "react-fast-compare": "^3.2.2",
37
37
  "release-it": "^19.1.0",
38
- "rollup": "^4.53.3",
38
+ "rollup": "^4.53.5",
39
39
  "shallow-equal-fuzzy": "^0.0.2",
40
40
  "tinybench": "^6.0.0",
41
41
  "typescript": "^5.9.3",
42
- "typescript-eslint": "^8.49.0",
42
+ "typescript-eslint": "^8.50.0",
43
43
  "underscore": "^1.13.7",
44
- "vite": "^7.2.7",
44
+ "vite": "^7.3.0",
45
45
  "vitest": "^4.0.15"
46
46
  },
47
47
  "engines": {
@@ -109,5 +109,5 @@
109
109
  "sideEffects": false,
110
110
  "type": "module",
111
111
  "types": "./index.d.ts",
112
- "version": "6.0.0-beta.1"
112
+ "version": "6.0.0-rc.1"
113
113
  }