Equality comparisons and sameness
JavaScript provides three different value-comparison operations:
===
— strict equality (triple equals)==
— loose equality (double equals)Object.is()
Which operation you choose depends on what sort of comparison you are looking to perform. Briefly:
- Double equals (
==
) will perform a type conversion when comparing two things, and will handleNaN
,-0
, and+0
specially to conform to IEEE 754 (soNaN != NaN
, and-0 == +0
); - Triple equals (
===
) will do the same comparison as double equals (including the special handling forNaN
,-0
, and+0
) but without type conversion; if the types differ,false
is returned. Object.is()
does no type conversion and no special handling forNaN
,-0
, and+0
(giving it the same behavior as===
except on those special numeric values).
They correspond to three of four equality algorithms in #"https://tc39.es/ecma262/multipage/abstract-operations.html#sec-islooselyequal">IsLooselyEqual:==
===
Object.is()
Note that the distinction between these all have to do with their handling of primitives; none of them compares whether the parameters are conceptually similar in structure. For any non-primitive objectsx
andy
which have the same structure but are distinct objects themselves, all of the above forms will evaluate tofalse
.
Strict equality using ===
Strict equality compares two values for equality. Neither value is implicitly converted to some other value before being compared. If the values have different types, the values are considered unequal. If the values have the same type, are not numbers, and have the same value, they're considered equal. Finally, if both values are numbers, they're considered equal if they're both notNaN
and are the same value, or if one is+0
and one is-0
.
const num = 0;const obj = new String("0");const str = "0";console.log(num === num); // trueconsole.log(obj === obj); // trueconsole.log(str === str); // trueconsole.log(num === obj); // falseconsole.log(num === str); // falseconsole.log(obj === str); // falseconsole.log(null === undefined); // falseconsole.log(obj === null); // falseconsole.log(obj === undefined); // false
Strict equality is almost always the correct comparison operation to use. For all values except numbers, it uses the obvious semantics: a value is only equal to itself. For numbers it uses slightly different semantics to gloss over two different edge cases. The first is that floating point zero is either positively or negatively signed. This is useful in representing certain mathematical solutions, but as most situations don't care about the difference between+0
and-0
, strict equality treats them as the same value. The second is that floating point includes the concept of a not-a-number value,NaN
, to represent the solution to certain ill-defined mathematical problems: negative infinity added to positive infinity, for example. Strict equality treatsNaN
as unequal to every other value — including itself. (The only case in which(x !== x)
istrue
is whenx
isNaN
.)
Besides===
, strict equality is also used by array index-finding methods includingArray.prototype.indexOf()
,Array.prototype.lastIndexOf()
,TypedArray.prototype.indexOf()
,TypedArray.prototype.lastIndexOf()
, andcase
-matching. This means you cannot useindexOf(NaN)
to find the index of aNaN
value in an array, or useNaN
as acase
value in aswitch
statement and make it match anything.
console.log([NaN].indexOf(NaN)); // -1switch (NaN) { case NaN: console.log("Surprise"); // Nothing is logged}
Loose equality using ==
Loose equality issymmetric:A == B
always has identical semantics toB == A
for any values ofA
andB
(except for the order of applied conversions). The behavior for performing loose equality using==
is as follows:
- If the operands have the same type, they are compared as follows:
- Object: return
true
only if both operands reference the same object. - String: return
true
only if both operands have the same characters in the same order. - Number: return
true
only if both operands have the same value.+0
and-0
are treated as the same value. If either operand isNaN
, returnfalse
; soNaN
is never equal toNaN
. - Boolean: return
true
only if operands are bothtrue
or bothfalse
. - BigInt: return
true
only if both operands have the same value. - Symbol: return
true
only if both operands reference the same symbol.
- Object: return
- If one of the operands is
null
orundefined
, the other must also benull
orundefined
to returntrue
. Otherwise returnfalse
. - If one of the operands is an object and the other is a primitive,convert the object to a primitive.
- At this step, both operands are converted to primitives (one of String, Number, Boolean, Symbol, and BigInt). The rest of the conversion is done case-by-case.
- If they are of the same type, compare them using step 1.
- If one of the operands is a Symbol but the other is not, return
false
. - If one of the operands is a Boolean but the other is not,convert the boolean to a number:
true
is converted to 1, andfalse
is converted to 0. Then compare the two operands loosely again. - Number to String:convert the string to a number. Conversion failure results in
NaN
, which will guarantee the equality to befalse
. - Number to BigInt: compare by their mathematical value. If the number is ±Infinity or
NaN
, returnfalse
. - String to BigInt: convert the string to a BigInt using the same algorithm as the
BigInt()
constructor. If conversion fails, returnfalse
.
Traditionally, and according to ECMAScript, all primitives and objects are loosely unequal toundefined
andnull
. But most browsers permit a very narrow class of objects (specifically, thedocument.all
object for any page), in some contexts, to act as if theyemulate the valueundefined
. Loose equality is one such context:null == A
andundefined == A
evaluate to true if, and only if, A is an object thatemulatesundefined
. In all other cases an object is never loosely equal toundefined
ornull
.
In most cases, using loose equality is discouraged. The result of a comparison using strict equality is easier to predict, and may evaluate more quickly due to the lack of type coercion.
The following example demonstrates loose equality comparisons involving the number primitive0
, the bigint primitive0n
, the string primitive'0'
, and an object whosetoString()
value is'0'
.
const num = 0;const big = 0n;const str = "0";const obj = new String("0");console.log(num == str); // trueconsole.log(big == num); // trueconsole.log(str == big); // trueconsole.log(num == obj); // trueconsole.log(big == obj); // trueconsole.log(str == obj); // true
Loose equality is only used by the==
operator.
Same-value equality using Object.is()
Same-value equality determines whether two values arefunctionally identical in all contexts. (This use case demonstrates an instance of theLiskov substitution principle.) One instance occurs when an attempt is made to mutate an immutable property:
// Add an immutable NEGATIVE_ZERO property to the Number constructor.Object.defineProperty(Number, "NEGATIVE_ZERO", { value: -0, writable: false, configurable: false, enumerable: false,});function attemptMutation(v) { Object.defineProperty(Number, "NEGATIVE_ZERO", { value: v });}
Object.defineProperty
will throw an exception when attempting to change an immutable property, but it does nothing if no actual change is requested. Ifv
is-0
, no change has been requested, and no error will be thrown. Internally, when an immutable property is redefined, the newly-specified value is compared against the current value using same-value equality.
Same-value equality is provided by theObject.is
method. It's used almost everywhere in the language where a value of equivalent identity is expected.
Same-value-zero equality
Similar to same-value equality, but +0 and -0 are considered equal.
Same-value-zero equality is not exposed as a JavaScript API, but can be implemented with custom code:
function sameValueZero(x, y) { if (typeof x === "number" && typeof y === "number") { // x and y are equal (may be -0 and 0) or they are both NaN return x === y || (x !== x && y !== y); } return x === y;}
Same-value-zero only differs from strict equality by treatingNaN
as equivalent, and only differs from same-value equality by treating-0
as equivalent to0
. This makes it usually have the most sensible behavior during searching, especially when working withNaN
. It's used byArray.prototype.includes()
,TypedArray.prototype.includes()
, as well asMap
andSet
methods for comparing key equality.
Comparing equality methods
People often compare double equals and triple equals by saying one is an "enhanced" version of the other. For example, double equals could be said as an extended version of triple equals, because the former does everything that the latter does, but with type conversion on its operands — for example,6 == "6"
. Alternatively, it can be claimed that double equals is the baseline, and triple equals is an enhanced version, because it requires the two operands to be the same type, so it adds an extra constraint.
However, this way of thinking implies that the equality comparisons form a one-dimensional "spectrum" where "totally strict" lies on one end and "totally loose" lies on the other. This model falls short withObject.is
, because it isn't "looser" than double equals or "stricter" than triple equals, nor does it fit somewhere in between (i.e., being both stricter than double equals, but looser than triple equals). We can see from the sameness comparisons table below that this is due to the way thatObject.is
handlesNaN
. Notice that ifObject.is(NaN, NaN)
evaluated tofalse
, wecould say that it fits on the loose/strict spectrum as an even stricter form of triple equals, one that distinguishes between-0
and+0
. TheNaN
handling means this is untrue, however. Unfortunately,Object.is
has to be thought of in terms of its specific characteristics, rather than its looseness or strictness with regard to the equality operators.
x | y | == | === | Object.is | SameValueZero |
---|---|---|---|---|---|
undefined | undefined | ✅ true | ✅ true | ✅ true | ✅ true |
null | null | ✅ true | ✅ true | ✅ true | ✅ true |
true | true | ✅ true | ✅ true | ✅ true | ✅ true |
false | false | ✅ true | ✅ true | ✅ true | ✅ true |
'foo' | 'foo' | ✅ true | ✅ true | ✅ true | ✅ true |
0 | 0 | ✅ true | ✅ true | ✅ true | ✅ true |
+0 | -0 | ✅ true | ✅ true | ❌ false | ✅ true |
+0 | 0 | ✅ true | ✅ true | ✅ true | ✅ true |
-0 | 0 | ✅ true | ✅ true | ❌ false | ✅ true |
0n | -0n | ✅ true | ✅ true | ✅ true | ✅ true |
0 | false | ✅ true | ❌ false | ❌ false | ❌ false |
"" | false | ✅ true | ❌ false | ❌ false | ❌ false |
"" | 0 | ✅ true | ❌ false | ❌ false | ❌ false |
'0' | 0 | ✅ true | ❌ false | ❌ false | ❌ false |
'17' | 17 | ✅ true | ❌ false | ❌ false | ❌ false |
[1, 2] | '1,2' | ✅ true | ❌ false | ❌ false | ❌ false |
new String('foo') | 'foo' | ✅ true | ❌ false | ❌ false | ❌ false |
null | undefined | ✅ true | ❌ false | ❌ false | ❌ false |
null | false | ❌ false | ❌ false | ❌ false | ❌ false |
undefined | false | ❌ false | ❌ false | ❌ false | ❌ false |
{ foo: 'bar' } | { foo: 'bar' } | ❌ false | ❌ false | ❌ false | ❌ false |
new String('foo') | new String('foo') | ❌ false | ❌ false | ❌ false | ❌ false |
0 | null | ❌ false | ❌ false | ❌ false | ❌ false |
0 | NaN | ❌ false | ❌ false | ❌ false | ❌ false |
'foo' | NaN | ❌ false | ❌ false | ❌ false | ❌ false |
NaN | NaN | ❌ false | ❌ false | ✅ true | ✅ true |
When to use Object.is() versus triple equals
In general, the only timeObject.is
's special behavior towards zeros is likely to be of interest is in the pursuit of certain meta-programming schemes, especially regarding property descriptors, when it is desirable for your work to mirror some of the characteristics ofObject.defineProperty
. If your use case does not require this, it is suggested to avoidObject.is
and use===
instead. Even if your requirements involve having comparisons between twoNaN
values evaluate totrue
, generally it is easier to special-case theNaN
checks (using theisNaN
method available from previous versions of ECMAScript) than it is to work out how surrounding computations might affect the sign of any zeros you encounter in your comparison.
Here's a non-exhaustive list of built-in methods and operators that might cause a distinction between-0
and+0
to manifest itself in your code:
-
(unary negation)Consider the following example:
jsconst stoppingForce = obj.mass * -obj.velocity;
If
obj.velocity
is0
(or computes to0
), a-0
is introduced at that place and propagates out intostoppingForce
.Math.atan2
,Math.ceil
,Math.pow
,Math.round
In some cases, it's possible for a
-0
to be introduced into an expression as a return value of these methods even when no-0
exists as one of the parameters. For example, usingMath.pow
to raise-Infinity
to the power of any negative, odd exponent evaluates to-0
. Refer to the documentation for the individual methods.Math.floor
,Math.max
,Math.min
,Math.sin
,Math.sqrt
,Math.tan
It's possible to get a
-0
return value out of these methods in some cases where a-0
exists as one of the parameters. E.g.,Math.min(-0, +0)
evaluates to-0
. Refer to the documentation for the individual methods.~
,<<
,>>
Each of these operators uses the ToInt32 algorithm internally. Since there is only one representation for 0 in the internal 32-bit integer type,
-0
will not survive a round trip after an inverse operation. E.g., bothObject.is(~~(-0), -0)
andObject.is(-0 << 2 >> 2, -0)
evaluate tofalse
.
Relying onObject.is
when the sign of zeros is not taken into account can be hazardous. Of course, when the intent is to distinguish between-0
and+0
, it does exactly what's desired.
Caveat: Object.is() and NaN
TheObject.is
specification treats all instances ofNaN
as the same object. However, sincetyped arrays are available, we can have distinct floating point representations ofNaN
which don't behave identically in all contexts. For example:
const f2b = (x) => new Uint8Array(new Float64Array([x]).buffer);const b2f = (x) => new Float64Array(x.buffer)[0];// Get a byte representation of NaNconst n = f2b(NaN);// Change the first bit, which is the sign bit and doesn't matter for NaNn[0] = 1;const nan2 = b2f(n);console.log(nan2); // NaNconsole.log(Object.is(nan2, NaN)); // trueconsole.log(f2b(NaN)); // Uint8Array(8) [0, 0, 0, 0, 0, 0, 248, 127]console.log(f2b(nan2)); // Uint8Array(8) [1, 0, 0, 0, 0, 0, 248, 127]