-
Notifications
You must be signed in to change notification settings - Fork 0
Equals
It is not exactly obvious in every case whether two JavaScript values should be considered value-equal, and in some cases there are good arguments both for and against. This document lists the algorithms used to determine value-equality in this module, notes any edge or unusual cases, and provides the rationale for any design decisions made. Note that, in this document, by an equals (or clone) implementation for a class, I mean the algorithm used to determine the results of calling equals (or clone) on an instance of that class (as the first operand, for equals).
-
The implementation used to determine whether
equals(x, y)is true is always the implementation for the type of the first operandx. The implementation fory's type is no case considered. This is different from Python, where the implementation fory's type might be considered when evaluatingx === yif the implementation forx's type returnsNotImplemented, or ify's type is a subtype forx. Indeed, there is no equivalent ofNotImplementedin this system, instead implementations must return a boolean value in all cases. -
As a general principle, value-equality is defined compositionally, so two complex values (i.e. objects) are value-equal if and only if all their parts (i.e. properties) are value-equal. In other words, value-equality is structural-equality for complex values. Note that this definition is recursive.
-
Another principle that value-equality algorithms in this module obey is that
x === yimpliesequals(x, y)(although, given how JavaScript works, it would be hard for this not to be the case). Note that this is not just an abstract rule, as theequalsfunction is defined to return early withtruein these cases. Moreover, SameValueZero(x, y) impliesequals(x, y), asequals(NaN, NaN)is true (see more below). -
Value-equality is intended to be an equivalence relation, so for all
x,y, andz, the following hold:- Reflexive:
equals(x, x) - Symmetrical:
equals(x, y)impliesequals(y, x) - Transitive:
equals(x, y)andequals(y, z)impliesequals(x, z)Note that this is unlike the===operator, which is not reflexive asNaN === NaNis false.
- Reflexive:
-
This module assumes that all code is written in 'safe' TypeScript, i.e. with strict type checking, no type errors, no
@ts-ignore, no casting toany, no non-thisreturns from constructors, etc. While this module should behave normally in most cases even in the presence such 'unsafe' TypeScript, no guarantees are made. While I am not aware of any errors which could be caused specifically by using this library with 'unsafe' TypeScript, undesired behaviour such as non-symmetrical value equality relations are quite possible. Note that 'unsafe' TypeScript includes monkeypatching new properties on classes and builtin objects. Such properties are not considered inequalsimplementations. -
This document does not discuss customization of
equalsimplementations in any detail. Documentation for customization ofequalsimplementations is here. -
Finally,
equalsandclonefrom this library are related so thatequals(x, clone(x))is always true (although this is more of a constraint oncloneimplementations thanequalsimplementations).
The symmetrical condition is important to consider when it comes to comparing values with different types. If x and y are of different types, then equals(x, y) and equals(y, x) will be determined by different implementations (x's type's and y's type's, respectively), which means that they may have different results, violating symmetry.
To avoid this result, equals(x, y) should only be true if x and y have exactly the same type, which is luckily what would be expected anyway (except perhaps in the case of subtypes, see design note below). This point is important to consider as it is quite possible to accidently violate it through inconsistent equals implementations on different classes which are related by inheritance. This is discussed in more detail below.
In general, equals defers to the JavaScript equality algorithm, as primitives in JavaScript are already value types. There are a few complications, mostly to do with wrapper classes, which are detailed in this section and the section on wrapper classes.
If x is a primitive of the type undefined, then equals(x, y) is true if and only if:
-
yisundefined.
If x is a primitive of the type null, then equals(x, y) is true if and only if:
-
yisnull.
DISCUSSION: Note that (despite the name) null-prototype objects are not instances of the type null. Thus, instances of a class C extends null and objects returned by Object.create(null) are not instances of null, and therefore this implementation does not apply to them.
If x is a primitive of the type boolean, then equals(x, y) is true if and only if:
-
x === yis true, OR -
yis aBooleanwrapper object, andx === y.valueOf()is true.
DISCUSSION: Note that (2) does not apply to Boolean wrapper subobjects. See below for more discussion on wrapper classes.
If x is a primitive of the type string, then equals(x, y) is true if and only if:
-
x === yis true, OR -
yis aStringwrapper object, andx === y.valueOf()is true.
DISCUSSION: Note that (2) does not apply to String wrapper subobjects. See below for more discussion on wrapper classes.
If x is a primitive of the type number, then equals(x, y) is true if and only if:
-
x === yis true, OR -
Number.isNaN(x)is true andNumber.isNaN(y)is true, OR -
yis aNumberwrapper object, andequals(x, y.valueOf())is true.
DISCUSSION: Note that, for numbers, equals uses the SameValueZero algorithm, which is the same as strict equality except NaNs equal other NaNs. As such equals(NaN, NaN) is true. Under both the SameValueZero algorithm and strict equality, -0 equals +0, so equals(0, -0) is true.
DESIGN NOTE: It's not exactly clear how to treat either negative zeros or NaNs in terms of value-equality, either in principle (i.e. what it really means for two expressions to have the same value) or in practice (i.e. what would be the most useful answer). Here is my reasoning for the algorithms I selected:
- For negative zeros, in principle both positive and negative zeros represent the same value (i.e. none of something) and therefore should be equal. In practice, treating positive and negative zeros as equal allows the useful
x === ytoequals(x, y)entailment to remain in effect.- For NaNs, its not clear that there's a principled answer to whether two NaNs have the same value. Theoretically, NaNs represent lacks of values, rather than values, and asking whether two lacks of values are the same value does not seem to be a coherent question. Thus, I decided that in practice it is most useful to have all NaNs compared equals so (for example) they can easily be grouped together in a partition of an array of numbers. This also has the useful consequence that
equals(unlike===) is reflexive.
DISCUSSION: Note that (3) does not apply to Number wrapper subobjects. See below for more discussion on wrapper classes.
If x is a primitive of the type BigInt then equals(x, y) is true if and only if:
-
x === yis true, OR -
yis aBigIntwrapper object, andequals(x, y.valueOf())is true.
DISCUSSION: Note that (2) does not apply to BigInt wrapper subobjects. See below for more discussion on wrapper classes.
If x is a primitive of the type symbol then equals(x, y) is true if and only if:
-
x === yis true, OR -
yis aSymbolwrapper object, andequals(x, y.valueOf())is true.
DISCUSSION: This means that equals(Symbol.for('x'), Symbol.for('x')) is true.
DISCUSSION: Note that (2) does not apply to Symbol wrapper subobjects. See below for more discussion on wrapper classes.
For the purposes of this library, an 'ordinary object' is a JavaScript object which is not:
- a function, OR
- an instance of a wrapper class, OR
- an exotic or builtin object, OR
- an instance of a class with a customized
equalsimplementation
In other words, ordinary objects are the objects that are left over when all of the other categories of objects have been excluded. Similarly, the equals implementation is, in a sense, the default implementation, which is overriden for specific categories of object.
DISCUSSION: Ordinary objects include both object literals and instances of uncustomized classes.
Under the default implementation, two ordinary objects are value-equal if and only if they have the exact same prototype, and all of their properties are value-equal. More specifically, if x is an ordinary ordinary object, then equals(x, y) is true if and only if:
-
xandyhave the same prototype (and thereforeyis also an ordinary object), AND - The number of public, own properties of
xis equal to the number of public, own properties ofy, AND - For every public, own property key
konx,-
kis an public, own property key ofy, AND -
equals(x[k], y[k])istrue
-
DESIGN NOTE: The limitation to own properties is trivial, because two objects are only value-equal if they have the same prototype, and if they have the same prototype then all their inherited properties are the same anyway.
DESIGN NOTE: It's not clear to me whether it would be better in principle to include or not include private properties in value-equality comparisons, but this doesn't matter as it is impossible to access them from outside the object itself, and therefore they cannot be included in practice.
Note that this implementation of equals is structural, in that two objects are equal only if all of their parts (i.e. properties) are the same. It is also recursive, as the equals function itself is used to compare the property values of the objects. That being said, it is not used to compare prototypes and property keys, which are instead compared using ===;
DESIGN NOTE: It might be suprising that the
equalsfunction is not also used to compare prototypes and property keys. For property keys, the reason is simple: the types of property keys (strings and symbols) are already compared using value-equality by===. For prototypes, the decision is not so clear. My thought was that, speaking abstractly, two values are value-equal if they are equivalent tokens of the same type. The type of an object in JavaScript is given by its prototype, therefore two values must have the same prototype to be value-equal. As the properties of prototypes are normally functions, and functions are considered value-equal only when reference-equal, most prototypes will only be value-equal when they are reference-equal anyway.
The prototype of every null-prototype object is null, therefore two null-prototype objects are value-equals if and only if they have value-equals own properties. Similarly, prototype of every object literal is Object.prototype, therefore two object literals are value-equals if and only if they have value-equals own properties.
Note that value-equality on the default implementation requires the prototypes of two objects to be identical for those objects to be equal. It is not enough for one object to be an instance of the prototype of the other object (i.e. for the prototype of one object to be in the prototype chain of the other). Thus, if x is an instance of a subclass of the class of y, then x is not value-equals to y.
DESIGN NOTE: This is perhaps not the most desirable or least suprising behavior that the implementation could have, but it is essential to maintain the symmetrical condition on value-equality. To see why, suppose this wasn't the case, and consider a class
Superand its subclassSub extends Super. SupposeSubhas a customized implementation ofequals, so that twoSubinstances are equal only if they are reference-equal (for more on this customization, see here). Now consider two objectssuperInstance = new Super()andsubInstance = new Sub(). To evaluateequals(superInstance, subInstance), we look toSuper'sequalsimplementation, which (in this hypothetical) is this alternative default implementation. OnSuper's implementation, we would first consider whether eitherSub.prototypeis in the prototype chain ofSuper.prototypeor vice versa.Sub.prototypeis in the prototype chain ofSuper.prototype(Sub.prototype.prototype === Super.prototype), thus (supposing neither of these objects have any addition properties),equals(superInstance, subInstance)is true. However, to evaluateequals(subInstance, superInstance), we look toSub'sequalsimplementation, which holds thatequals(subInstance, superInstance)is false. In this way, we get a violation of symmetry.
Objects with circular references can be compared for value-equality. Consider the membership graph of an object x, where the nodes are made up of x and all the objects transitively related to x as property values, and the edges reflect each property value relation directed from object to property. Objects with circular references will then have cyclic membership graphs. x will compare as value-equals to an object y if and only if y has the same membership graph as x, and each node in x's graph is value-equals to the equivalent node in y's graph.
If x is an instance of Function (and does not have a customized equals implementation),1 then equals(x, y) is true if and only if:
-
x === yis true. In other words, two functions will be considered value-equals if and only if they are reference-equals.
DESIGN NOTE: Let us call two functions which always return the same value and have the same side-effects given the same arguments extensionally-equivalent. Ideally, two functions would be considered value-equal if and only if they were extensionally-equivalent. However, determining whether two functions are extensionally-equivalent in general is considered computational infeasible to say the least.2 In principle, the source code of functions could be analyzed to determine whether functions are extensionally-equivalent in at least some cases, but this would be both computationally costly and out-of-scope of this project. As there is no practical way of comparing the 'value' of functions, comparing them be reference is the best we can do.
The wrapper classes are the classes: Boolean, Number, String, BigInt or Symbol. They are so-called because their instances 'wrap' a primitive value. I will refer to any object x such that x instanceof P is true for some wrapper class P as a primitive wrapper. The equals implementations of primitive wrappers depends on whether they are direct or indirect instances of a wrapper class.
A wrapper object is a direct instance of a wrapper class. More specifically, a wrapper object is one of the following types of object:
-
Booleanwrapper objects-
new Boolean(x), wherexis any value -
Object(x), wherexis a boolean
-
-
Numberwrapper objects-
new Number(x), wherexis any value except a BigInt, symbol or an object which converts into a BigInt or symbol -
Object(x), wherexis a number
-
-
Stringwrapper objects-
new String(x), wherexis any value except a symbol or an object which converts into a symbol -
Object(x), wherexis a string
-
-
BigIntwrapper objects:Object(x), wherexis a BigInt -
Symbolwrapper objects:Object(x), wherexis a symbol Note that there are noundefinedornullwrapper objects.3 Each type of wrapper object wraps a primitive value of the relevant type.
Objects with a wrapper object in their prototype chain, i.e. instances of wrapper subclasses, have a different equals implementation than wrapper objects, for reasons I will explain below. I will call these objects wrapper subobjects. Together, wrapper objects and wrapper subobjects make up all the primitive wrappers.
In general, wrapper objects are value-equals to both other wrapper objects with the same wrapped value and to the wrapped value itself. As a formula, where P is a wrapped class and x and y are primitives of the type wrapped by P, equals(x, y) implies equals(new P(x), x), equals(new P(x), y), and equals(new P(x), new P(y)).
This does perhaps violate the principle that equals(x, y) should only be true if x and y have exactly the same type, as primitives and wrapper objects do not have the same type, but any violation of symmetry is avoided as primitives and wrapper object equivalents have the same equals implementations.
Note that this does not apply to wrapper subobjects, for reasons explained below.
DESIGN NOTE: In this way
equalsis unlike===, as wrapper objects are objects in JavaScript, and as such two wrapper objects are equal if and only if they are reference-equal. It seems clear enough that the 'value' of a wrapper object is the primitive it wraps, and therefore two wrapper objects which wrap the same primitive should be considered value-equals. Less clear is whether a wrapper object should be considered value-equal to the primitive it wraps. While it is true that these values have, strictly speaking, different types, their behavior is almost identical, and what differences there are seem to be mostly implementation details. As such, I think it makes sense to treat them as value-equals.
If x is a Boolean wrapper object, then equals(x, y) is true if and only if:
-
yis aBooleanwrapper object, andequals(x.valueOf(), y.valueOf())is true, OR -
yis a boolean, andequals(x.valueOf(), y)is true.
DISCUSSION: Note that (1) does not apply to Boolean wrapper subobjects. See below for more discussion on wrapper subobjects.
If x is a String wrapper object, then equals(x, y) is true if and only if:
-
yis aStringwrapper object, andequals(x.valueOf(), y.valueOf())is true, OR -
yis a string, andequals(x.valueOf(), y)is true.
DISCUSSION: Note that (1) does not apply to String wrapper subobjects. See below for more discussion on wrapper subobjects.
If x is a Number wrapper object, then equals(x, y) is true if and only if:
-
yis aNumberwrapper object, andequals(x.valueOf(), y.valueOf())is true, OR -
yis a number, andequals(x.valueOf(), y)is true.
DISCUSSION: Note that (1) does not apply to Number wrapper subobjects. See below for more discussion on wrapper subobjects.
If x is a BigInt wrapper object, then equals(x, y) is true if and only if:
-
yis aBigIntwrapper object, andequals(x.valueOf(), y.valueOf())is true, OR -
yis a BigInt, andequals(x.valueOf(), y)is true.
DISCUSSION: Note that (1) does not apply to BigInt wrapper subobjects. See below for more discussion on wrapper subobjects.
If x is a Symbol wrapper object, then equals(x, y) is true if and only if:
-
yis aSymbolwrapper object, andequals(x.valueOf(), y.valueOf())is true, OR -
yis a symbol, andequals(x.valueOf(), y)is true.
DISCUSSION: Note that (1) does not apply to Symbol wrapper subobjects. See below for more discussion on wrapper subobjects.
The equals implementation for wrapper subobjects is the same as the default implementation with the additional condition that the primitives that the wrapper subobjects wrap must also be value-equals. The idea is that a wrapper subobject's wrapped primitive functions essentially like a property of the subobject.
More specifically, if x is a wrapper subobject, and x does not have a customized equals implementation, then equals(x, y) is true if and only if:
-
xandyhave the same prototype (and thereforeyis also a wrapper subobject), AND - The number of public, own properties of
xis equal to the number of public, own properties ofy, AND - For every public, own property key
konx,-
kis an public, own property key ofy, AND -
equals(x[k], y[k])istrue,
-
- AND IF
x.valueOfis a function (and thereforey.valueOfis a function)4 THENequals(x.valueOf(), y.valueOf())istrue.
valueOf serves as the way in which wrapped primitives of wrapper subobjects are accessed. This method could be overridden if you wanted to compare a different value as the wrapper subobject's wrapped primitive (although the wrapper subobject's actual wrapped primitive would then be inaccessible). If the override of valueOf is not a function then it is ignored.
Unlike wrapper objects, wrapper subobjects do not compare equal with either their wrapped primitive, nor wrapper objects which wrap the same primitive value. This is because neither the equals implementation of primitives nor the equals implementation of wrapper objects considers any properties that a wrapped subobject may have in addition to the primitive they wrap. As such, if wrapper subobjects compared equal with either their wrapped primitive or a wrapper object which wrapped the same primitive value, this could lead to violations of symmetry.5
DESIGN NOTE: It would be possible for wrapper subobjects to be value-equal to primitive values and wrapper objects if the
equalsimplementation for primitive values and wrapper objects considered the existence of other properties on the value being compared to. I did not persue this option for the following reasons
- It would only apply to wrapper subobjects which have no additional properties, and it's not clear why anyone would use such a subobject instead of just a wrapper object.
- It would add additional checks which wouldn't be needed in the vast majority of cases, creating an unnecessary slowdown.
- It would not help with wrapper subobjects with customized
equalsimplementation, as discussed below.
Wrapper subobjects can be created by subclassing Boolean, Number or String (i.e. classes class C extends Boolean, class C extends Number, class C extends String). BigInt and Symbol cannot be subclassed in this way in JavaScript. Like regular classes, these wrapper subclasses can have customized equals implementations, although these can function differently that for ordinary classes. For more on this, see here.
The general form of equals implementations for built-in classes C, and its subclasses, is as follows: If x is an instance of C, and x does not have a customized equals implementation, then equals(x, y) is true if and only if:
-
xandyhave the same prototype (and thereforeyis a fortiori also an instance ofC), AND -
xandyshare certain other,C-specific properties
Every subclass of a built-in class, unless it has a customized equals implementation, will then inherit the built-in class' equals implementation. For the most part, I believe this is desirable behavior: if you subclass (e.g) Set, then you probably want instances of this subclass to be compared as value-equal only if they have all the same members. If a different equals implementation is desired for a built-in subclass then it can be customized.
Note, however, that two instances of a built-in class C will only compare value-equal if they have the exact same prototype. Thus, instances of a built-in class will not compare value-equal to instances of one of its subclasses, and instances of different subclasses of the same built-in class will not compare value-equal to each other. The reason for this is to avoid violations of symmetry.6
If x is an instance of Array (and does not have a customized equals implementation), then equals(x, y) is true if and only if:
-
xandyhave the same prototype, AND -
x.lengthequalsy.length, AND - For every
iin the range0tox.length - 1,equals(x[i], y[i])is true.
If x is an instance of Set (and does not have a customized equals implementation), then equals(x, y) is true if and only if:
-
xandyhave the same prototype, AND -
x.sizeequalsy.size, AND - For every member
xmofx, there is some memberymofysuch thatequals(xm, ym)is true, AND - For every member
ymofy, there is some memberxmofxsuch thatequals(xm, ym)is true
DESIGN NOTE: Condition 4 may seem redundant, but consider the fact that a
Setmay have two (or more) members that are value-equals. A singleymmay then satisfy Condition 3 for multiple value-equals members ofx, which would then possibly allow for other members ofyto be value-unequal to every member ofx. For example, considerx = new Set([1], [1])andy = new Set([1], [2]). Conditions 1, 2 and 3 are true ofxandy, but the sets are not value-equals. For the value-equality of two sets, we want a one-to-one relation between value-equal members of the two sets to exist, so Condition 4 is needed.
If x is an instance of Map (and does not have a customized equals implementation), then equals(x, y) is true if and only if:
-
xandyhave the same prototype, AND -
x.sizeequalsy.size, AND - For every key
xkofx, there is some keyykofysuch that-
equals(xk, yk)is true, AND -
equals(x[xk], y[yk])is true
-
- For every key
ykofy, there is some keyxkofxsuch that-
equals(xk, yk)is true, AND -
equals(x[xk], y[yk])is true
-
DESIGN NOTE: Condition 4 may seem redundant, but consider the fact that a
Mapmay have two (or more) keys that are value-equals with value-equals values. A singleykmay then satisfy Condition 3 for multiple value-equals keys and values ofx, which would then possibly allow for other keys ofyto be value-unequal to every key ofx. For the value-equality of two maps, we want a one-to-one relation between value-equal key/value pairs of the two maps to exist, so Condition 4 is needed.
If x is an instance of RegExp (and does not have a customized equals implementation), then equals(x, y) is true if and only if:
-
xandyhave the same prototype, AND -
x.toString() === y.toString()is true
The string representation of a RegExp includes both its source and flags properties, and as such two regexps with the same source and flags will have the same string representation.
DESIGN NOTE: The string representations of
xandydo not need to compared byequalsas they are strings, thereforex.toString() === y.toString()is the same asequals(x.toString(), y.toString()).
DESIGN NOTE: The
equalsrepresentation forRegExponly compares the direct string representation of the regular expressions, and not the underlying sets of strings theRegExps match. For example,equals(/^\d$/, /^[^\D]$/)is false, even though these two regular expressions match all and only the same strings (specifically, the strings with exactly 1 digit and nothing else). Ideally, value-equality would compareRegExps in terms of the strings they match, but it seems computationally infeasible to do this in the general case.
If x is an instance of Date (and does not have a customized equals implementation), then equals(x, y) is true if and only if:
-
xandyhave the same prototype, AND -
x.valueOf() === y.valueOf()is true
DESIGN NOTE: The primitive value conversion of a
Datereturns its timestamp, and as such two dates representing the same moment in time will have the same primitive value conversion. The timestamps ofxandydo not need to compared byequalsas they are numbers, thereforex.valueOf() === y.valueOf()is the same asequals(x.valueOf(), y.valueOf()).
If x is an instance of ArrayBuffer or SharedArrayBuffer (and does not have a customized equals implementation), then equals(x, y) is true if and only if:
-
xandyhave the same prototype, AND -
x.byteLengthequalsy.byteLength, AND - With
xArray = new Uint8Array(x)andyArray = new Uint8Array(y), for everyiin the range0tox.byteLength - 1,xArray[i] === yArray[i]is true.
DESIGN NOTE:
Uint8Arrays are used here as the 'members' of array buffers cannot be accessed directly.
DESIGN NOTE: The members of array buffers do not need to compared by
equalsas they are numbers.
DESIGN NOTE: This implementation does not consider whether or not an array buffer is resizable when it comes comparing array buffers for value-equality. While it would be easily technically to add such a check, resizability does not seem to be essential to the value.
If x is an instance of DataView (and does not have a customized equals implementation), then equals(x, y) is true if and only if:
-
xandyhave the same prototype, AND -
x.byteLengthequalsy.byteLength, AND - For every
iin the range0tox.byteLength - 1,x.getInt8(i) === y.getInt8(i)is true.
DESIGN NOTE: The 'members' of data views do not need to compared by
equalsas they are numbers.
DESIGN NOTE: This implementation does not consider whether or not a data view is length-tracking when it comes to comparing array buffers for value-equality. Practically speaking, whether or not a given data view is length-tracking cannot be determined from the data view itself, other than by editing the actual data view, which seems inappropriate for an equality-checking function. Luckily, length-tracking does not seem to be essential to the value, so not being able to access it will not be a problem.
If x is an instance of a TypedArray class, i.e. Int8Array, Uint8Array, Uint8ClampedArray, Int16Array, Uint16Array, Int32Array, Uint32Array, Float32Array, Float64Array, BigInt64Array, BigUint64Array, and does not have a customized equals implementation, then equals(x, y) is true if and only if:
-
xandyhave the same prototype (which means thatxandymust be of the same type of typed array, e.g. they must both beInt8Arrays), AND -
x.byteLengthequalsy.byteLength, AND - For every
iin the range0tox.byteLength - 1,x[i] === y[i]is true.
DESIGN NOTE: The 'members' of typed arrays do not need to compared by
equalsas they are numbers.
DESIGN NOTE: This implementation does not consider whether or not a typed array is length-tracking when it comes to comparing typed arrays for value-equality. Practically speaking, whether or not a given typed array is length-tracking cannot be determined from the typed array itself, other than by editing the actual typed array, which seems inappropriate for an equality-checking function. Luckily, length-tracking does not seem to be essential to the value, so not being able to access it will not be a problem.
If x is an instance of WeakRef (and does not have a customized equals implementation), then equals(x, y) is true if and only if:
-
xandyhave the same prototype, AND -
equals(x.deref(), y.deref())is true.
Note that, if the target of x has been reclaimed, then x.deref() is undefined. This means that all WeakRefs with reclaimed targets are value-equals.
If x is an instance of WeakSet (and does not have a customized equals implementation), then equals(x, y) is true if and only if:
-
x === yis true. In other words, two weak sets will be considered value-equals if and only if they are reference-equals.
DESIGN NOTE: Ideally, at least for the purposes of value comparison, we would determine whether two weak sets are value-equal by comparing the members of the weak sets. Practically speaking, this is impossible, as there is no way to iterate through the members of a weak set. Thus, reference equality is the best that can be done to compare weak sets.
If x is an instance of WeakMap (and does not have a customized equals implementation),
then equals(x, y) is true if and only if:
-
x === yis true. In other words, two weak maps will be considered value-equals if and only if they are reference-equals.
DESIGN NOTE: Ideally, at least for the purposes of value comparison, we would determine whether two weak maps are value-equal by comparing the keys and values of the weak maps. Practically speaking, this is impossible, as there is no way to iterate through the keys and values of a weak map. Thus, reference equality is the best that can be done to compare weak maps.
If x is an instance of Promise (and does not have a customized equals implementation),
then equals(x, y) is true if and only if:
-
x === yis true. In other words, two promises will be considered value-equals if and only if they are reference-equals.
DESIGN NOTE: Ideally, we would consider two promises to be value-equals if they both have the same eventual state. However, we cannot block execution on an equality check, nor can we return a promise from an equality check, so this is impossible in practice. Less ideally, but still better than the reference-equals implementation, would be to check that the promises both have the same executor function. Unfortunately, it is not possible to access the executor of a promise, so this implementation is also practically impossible.
The equals implementations for classes, including subclasses of built-in classes and wrapper subclasses of Boolean, Number, and String, can be customized using the @customize decorators. For more details, see here.
1 As discussed here, customized Function subclasses have basically the same equals semantics as non-customized subclasses.
2 e.g. "comparing the equality of functions is generally considered computationally intractable ... The kind of equality we are referring to here is "value equality," and opposed to the "pointer equality" found, for example, with Java's ==." (A Gentle Introduction to Haskell). I believe the problem of determining whether two functions are extensionally-equivalent can be reduced to the halting problem, by Rice's theorem, but even if the problem is solvable in general it is clearly more computationally expensive than would be expected for an equality-comparison function.
3 So-called null prototype objects, i.e. Object.create(null), { __proto__: null }, instances of class C extends null, and other objects with these objects in their prototype chain, are not primitive wrappers (and therefore not wrapper objects), as x instanceof null is not true (or even well-formed) for these objects.
4 Note that, if valueOf is an own property of x then it must also be a value-equal own property of y and therefore also a function on y, and if it is an inherited property of x then it is on the prototype shared by x and y, and therefore also a function on y.
5 For example, consider wrap = Object(true) and subwrap = Object.create(Object(true), { field: { value: 0 } }). equals(subwrap, wrap) is false, as wrap does not have a field property. However, if wrapper subobjects could compare as value-equal to object wrappers, then equals(wrap, subwrap) would be true, as wrap and subwrap wrap the same primitive value (true) and the equals implementation for object wrappers does not consider other properties. The same is true for wrapper subobjects and primitive values.
6 For example, consider the class @customize.equals('ref') class C extends Set and the objects bi = new Set() and sub = new C(). equals(sub, bi) is false, as sub uses reference semantics for its equals implementation and sub and bi are different objects. However, if instances of built-in subclasses could compare value-equals to built-in objects, then equals(bi, sub) would be true, as sub and bi are both instances of Sub and both have the same members (i.e. none).