Skip to content
simonvarey edited this page Jan 9, 2026 · 5 revisions

Algorithms for the equals Function for Primitive, Builtin and User-defined Types

Introduction

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).

General Principles

  • The implementation used to determine whether equals(x, y) is true is always the implementation for the type of the first operand x. The implementation for y's type is no case considered. This is different from Python, where the implementation for y's type might be considered when evaluating x === y if the implementation for x's type returns NotImplemented, or if y's type is a subtype for x. Indeed, there is no equivalent of NotImplemented in 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 === y implies equals(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 the equals function is defined to return early with true in these cases. Moreover, SameValueZero(x, y) implies equals(x, y), as equals(NaN, NaN) is true (see more below).

  • Value-equality is intended to be an equivalence relation, so for all x, y, and z, the following hold:

    • Reflexive: equals(x, x)
    • Symmetrical: equals(x, y) implies equals(y, x)
    • Transitive: equals(x, y) and equals(y, z) implies equals(x, z) Note that this is unlike the === operator, which is not reflexive as NaN === NaN is false.
  • 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 to any, no non-this returns 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 in equals implementations.

  • This document does not discuss customization of equals implementations in any detail. Documentation for customization of equals implementations is here.

  • Finally, equals and clone from this library are related so that equals(x, clone(x)) is always true (although this is more of a constraint on clone implementations than equals implementations).

Symmetry and Comparisons Across Different Types

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.

Implementations for Primitives

Introduction

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.

undefined Implementation

If x is a primitive of the type undefined, then equals(x, y) is true if and only if:

  1. y is undefined.

null Implementation

If x is a primitive of the type null, then equals(x, y) is true if and only if:

  1. y is null.

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.

boolean Implementation

If x is a primitive of the type boolean, then equals(x, y) is true if and only if:

  1. x === y is true, OR
  2. y is a Boolean wrapper object, and x === y.valueOf() is true.

DISCUSSION: Note that (2) does not apply to Boolean wrapper subobjects. See below for more discussion on wrapper classes.

string Implementation

If x is a primitive of the type string, then equals(x, y) is true if and only if:

  1. x === y is true, OR
  2. y is a String wrapper object, and x === y.valueOf() is true.

DISCUSSION: Note that (2) does not apply to String wrapper subobjects. See below for more discussion on wrapper classes.

number Implementation

If x is a primitive of the type number, then equals(x, y) is true if and only if:

  1. x === y is true, OR
  2. Number.isNaN(x) is true and Number.isNaN(y) is true, OR
  3. y is a Number wrapper object, and equals(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 === y to equals(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.

BigInt Implementation

If x is a primitive of the type BigInt then equals(x, y) is true if and only if:

  1. x === y is true, OR
  2. y is a BigInt wrapper object, and equals(x, y.valueOf()) is true.

DISCUSSION: Note that (2) does not apply to BigInt wrapper subobjects. See below for more discussion on wrapper classes.

symbol Implementation

If x is a primitive of the type symbol then equals(x, y) is true if and only if:

  1. x === y is true, OR
  2. y is a Symbol wrapper object, and equals(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.

Implementation for Ordinary Objects

What are Ordinary Objects (in this Toolkit)?

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 equals implementation

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.

The Default Implementation: The Implementation for Ordinary Objects

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:

  1. x and y have the same prototype (and therefore y is also an ordinary object), AND
  2. The number of public, own properties of x is equal to the number of public, own properties of y, AND
  3. For every public, own property key k on x,
    • k is an public, own property key of y, AND
    • equals(x[k], y[k]) is true

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 equals function 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 Super and its subclass Sub extends Super. Suppose Sub has a customized implementation of equals, so that two Sub instances are equal only if they are reference-equal (for more on this customization, see here). Now consider two objects superInstance = new Super() and subInstance = new Sub(). To evaluate equals(superInstance, subInstance), we look to Super's equals implementation, which (in this hypothetical) is this alternative default implementation. On Super's implementation, we would first consider whether either Sub.prototype is in the prototype chain of Super.prototype or vice versa. Sub.prototype is in the prototype chain of Super.prototype (Sub.prototype.prototype === Super.prototype), thus (supposing neither of these objects have any addition properties), equals(superInstance, subInstance) is true. However, to evaluate equals(subInstance, superInstance), we look to Sub's equals implementation, which holds that equals(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.

Implementation for Functions

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:

  1. x === y is 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.

Implementations for Wrapper Classes

What are Wrapper Classes and Wrapper Objects?

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:

  • Boolean wrapper objects
    • new Boolean(x), where x is any value
    • Object(x), where x is a boolean
  • Number wrapper objects
    • new Number(x), where x is any value except a BigInt, symbol or an object which converts into a BigInt or symbol
    • Object(x), where x is a number
  • String wrapper objects
    • new String(x), where x is any value except a symbol or an object which converts into a symbol
    • Object(x), where x is a string
  • BigInt wrapper objects: Object(x), where x is a BigInt
  • Symbol wrapper objects: Object(x), where x is a symbol Note that there are no undefined or null wrapper 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.

Implementations for Wrapper Objects

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 equals is 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.

Boolean Wrapper Object Implementation

If x is a Boolean wrapper object, then equals(x, y) is true if and only if:

  1. y is a Boolean wrapper object, and equals(x.valueOf(), y.valueOf()) is true, OR
  2. y is a boolean, and equals(x.valueOf(), y) is true.

DISCUSSION: Note that (1) does not apply to Boolean wrapper subobjects. See below for more discussion on wrapper subobjects.

String Wrapper Object Implementation

If x is a String wrapper object, then equals(x, y) is true if and only if:

  1. y is a String wrapper object, and equals(x.valueOf(), y.valueOf()) is true, OR
  2. y is a string, and equals(x.valueOf(), y) is true.

DISCUSSION: Note that (1) does not apply to String wrapper subobjects. See below for more discussion on wrapper subobjects.

Number Wrapper Object Implementation

If x is a Number wrapper object, then equals(x, y) is true if and only if:

  1. y is a Number wrapper object, and equals(x.valueOf(), y.valueOf()) is true, OR
  2. y is a number, and equals(x.valueOf(), y) is true.

DISCUSSION: Note that (1) does not apply to Number wrapper subobjects. See below for more discussion on wrapper subobjects.

BigInt Wrapper Object Implementation

If x is a BigInt wrapper object, then equals(x, y) is true if and only if:

  1. y is a BigInt wrapper object, and equals(x.valueOf(), y.valueOf()) is true, OR
  2. y is a BigInt, and equals(x.valueOf(), y) is true.

DISCUSSION: Note that (1) does not apply to BigInt wrapper subobjects. See below for more discussion on wrapper subobjects.

Symbol Wrapper Object Implementation

If x is a Symbol wrapper object, then equals(x, y) is true if and only if:

  1. y is a Symbol wrapper object, and equals(x.valueOf(), y.valueOf()) is true, OR
  2. y is a symbol, and equals(x.valueOf(), y) is true.

DISCUSSION: Note that (1) does not apply to Symbol wrapper subobjects. See below for more discussion on wrapper subobjects.

Implementations for 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:

  1. x and y have the same prototype (and therefore y is also a wrapper subobject), AND
  2. The number of public, own properties of x is equal to the number of public, own properties of y, AND
  3. For every public, own property key k on x,
    • k is an public, own property key of y, AND
    • equals(x[k], y[k]) is true,
  4. AND IF x.valueOf is a function (and therefore y.valueOf is a function)4 THEN equals(x.valueOf(), y.valueOf()) is true.

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 equals implementation 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

  1. 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.
  2. It would add additional checks which wouldn't be needed in the vast majority of cases, creating an unnecessary slowdown.
  3. It would not help with wrapper subobjects with customized equals implementation, 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.

Implementations for Built-in Classes

Introduction

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:

  1. x and y have the same prototype (and therefore y is a fortiori also an instance of C), AND
  2. x and y share 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

Array Implementation

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:

  1. x and y have the same prototype, AND
  2. x.length equals y.length, AND
  3. For every i in the range 0 to x.length - 1, equals(x[i], y[i]) is true.

Set Implementation

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:

  1. x and y have the same prototype, AND
  2. x.size equals y.size, AND
  3. For every member xm of x, there is some member ym of y such that equals(xm, ym) is true, AND
  4. For every member ym of y, there is some member xm of x such that equals(xm, ym) is true

DESIGN NOTE: Condition 4 may seem redundant, but consider the fact that a Set may have two (or more) members that are value-equals. A single ym may then satisfy Condition 3 for multiple value-equals members of x, which would then possibly allow for other members of y to be value-unequal to every member of x. For example, consider x = new Set([1], [1]) and y = new Set([1], [2]). Conditions 1, 2 and 3 are true of x and y, 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.

Map Implementation

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:

  1. x and y have the same prototype, AND
  2. x.size equals y.size, AND
  3. For every key xk of x, there is some key yk of y such that
    • equals(xk, yk) is true, AND
    • equals(x[xk], y[yk]) is true
  4. For every key yk of y, there is some key xk of x such 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 Map may have two (or more) keys that are value-equals with value-equals values. A single yk may then satisfy Condition 3 for multiple value-equals keys and values of x, which would then possibly allow for other keys of y to be value-unequal to every key of x. 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.

RegExp Implementation

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:

  1. x and y have the same prototype, AND
  2. 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 x and y do not need to compared by equals as they are strings, therefore x.toString() === y.toString() is the same as equals(x.toString(), y.toString()).

DESIGN NOTE: The equals representation for RegExp only compares the direct string representation of the regular expressions, and not the underlying sets of strings the RegExps 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 compare RegExps in terms of the strings they match, but it seems computationally infeasible to do this in the general case.

Date Implementation

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:

  1. x and y have the same prototype, AND
  2. x.valueOf() === y.valueOf() is true

DESIGN NOTE: The primitive value conversion of a Date returns its timestamp, and as such two dates representing the same moment in time will have the same primitive value conversion. The timestamps of x and y do not need to compared by equals as they are numbers, therefore x.valueOf() === y.valueOf() is the same as equals(x.valueOf(), y.valueOf()).

ArrayBuffer and SharedArrayBuffer Implementations

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:

  1. x and y have the same prototype, AND
  2. x.byteLength equals y.byteLength, AND
  3. With xArray = new Uint8Array(x) and yArray = new Uint8Array(y), for every i in the range 0 to x.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 equals as 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.

DataView Implementation

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:

  1. x and y have the same prototype, AND
  2. x.byteLength equals y.byteLength, AND
  3. For every i in the range 0 to x.byteLength - 1, x.getInt8(i) === y.getInt8(i) is true.

DESIGN NOTE: The 'members' of data views do not need to compared by equals as 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.

TypedArray Implementation

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:

  1. x and y have the same prototype (which means that x and y must be of the same type of typed array, e.g. they must both be Int8Arrays), AND
  2. x.byteLength equals y.byteLength, AND
  3. For every i in the range 0 to x.byteLength - 1, x[i] === y[i] is true.

DESIGN NOTE: The 'members' of typed arrays do not need to compared by equals as 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.

WeakRef Implementation

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:

  1. x and y have the same prototype, AND
  2. 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.

WeakSet Implementation

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:

  1. x === y is 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.

WeakMap Implementation

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:

  1. x === y is 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.

Promise Implementation

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:

  1. x === y is 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.

Customization

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.

Footnotes

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).