ES6 has three built-in facilities for determining whether some x and some y are "the same". They are: equality or "double equals" (==
), strict equality or "triple equals" (===
), and Object.is
. (Note that Object.is
was added in ES6. Both double equals and triple equals existed prior to ES6, and their behavior remains unchanged.)
Overview
For demonstration, here are the three sameness comparisons in use:
x == y
x === y
Object.is(x, y)
Briefly, double equals will perform a type conversion when comparing two things; triple equals will do the same comparison without type conversion (by simply always returning false
if the types differ); and Object.is
will behave the same way as triple equals, but with special handling for NaN
and -0
and +0
so that the last two are not said to be the same, while Object.is(NaN, NaN)
will be true
. (Comparing NaN
with NaN
ordinarily—i.e., using either double equals or triple equals—evaluates to false
, because IEEE 754 says so.)
Do 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 objects x and y which have the same structure but are distinct objects themselves, all of the above forms will evaluate to false
.
For example:
let x = { value: 17 }; let y = { value: 17 }; console.log(Object.is(x, y)); // false; console.log(x === y); // false console.log(x == y); // false
Abstract equality, strict equality, and same value
In ES5, the comparison performed by ==
is described in Section 11.9.3, The Abstract Equality Algorithm. The ===
comparison is 11.9.6, The Strict Equality Algorithm. (Go look at these. They're brief and readable. Hint: read the strict equality algorithm first.) ES5 also describes, in Section 9.12, The SameValue Algorithm for use internally by the JS engine. It's largely the same as the Strict Equality Algorithm, except that 11.9.6.4 and 9.12.4 differ in handling Number
s. ES6 simply proposes to expose this algorithm through Object.is
.
We can see that with double and triple equals, with the exception of doing a type check upfront in 11.9.6.1, the Strict Equality Algorithm is a subset of the Abstract Equality Algorithm, because 11.9.6.2–7 correspond to 11.9.3.1.a–f.
A model for understanding equality comparisons?
Prior to ES6, you might have said of double equals and triple equals that one is an "enhanced" version of the other. For example, someone might say that double equals is an extended version of triple equals, because the former does everything that the latter does, but with type conversion on its operands. E.g., 6 == "6"
. (Alternatively, someone might say 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. Which one is the better model for understanding depends on how you choose to view things.)
However, this way of thinking about the built-in sameness operators is not a model that can be stretched to allow a place for ES6's Object.is
on this "spectrum". Object.is
isn't simply "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 that Object.is
handles NaN
. Notice that if Object.is(NaN, NaN)
evaluated to false
, we could say that it fits on the loose/strict spectrum as an even stricter form of triple equals, one that distinguishes between -0
and +0
. The NaN
handling means this is untrue, however. Unfortunately, Object.is
simply 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 |
---|---|---|---|---|
undefined |
undefined |
true |
true |
true |
null |
null |
true |
true |
true |
true |
true |
true |
true |
true |
false |
false |
true |
true |
true |
"foo" |
"foo" |
true |
true |
true |
{ foo: "bar" } |
x |
true |
true |
true |
0 |
0 |
true |
true |
true |
+0 |
-0 |
true |
true |
false |
0 |
false |
true |
false |
false |
"" |
false |
true |
false |
false |
"" |
0 |
true |
false |
false |
"0" |
0 |
true |
false |
false |
"17" |
17 |
true |
false |
false |
[1,2] |
"1,2" |
true |
false |
false |
new String("foo") |
"foo" |
true |
false |
false |
null |
undefined |
true |
false |
false |
null |
false |
false |
false |
false |
undefined |
false |
false |
false |
false |
{ foo: "bar" } |
{ foo: "bar" } |
false |
false |
false |
new String("foo") |
new String("foo") |
false |
false |
false |
0 |
null |
false |
false |
false |
0 |
NaN |
false |
false |
false |
"foo" |
NaN |
false |
false |
false |
NaN |
NaN |
false |
false |
true |
When to use Object.is
versus triple equals
Aside from the way it treats NaN
, generally, the only time Object.is
's special behavior towards zeroes is likely to be of interest is in the pursuit of certain metaprogramming schemes, especially regarding property descriptors when it is desirable for your work to mirror some of the characteristics of Object.defineProperty
. If your use case does not require this, it is suggested to avoid Object.is
and use ===
instead. Even if your requirements involve having comparisons between two NaN
values evaluate to true
, generally it is easier to special-case the NaN
checks (using the isNaN
method available from previous versions of ECMAScript) than it is to work out how surrounding computations might affect the sign of any zeroes you encounter in your comparison.
Here's an inexhaustive list of built-in methods and operators that might cause a distinction between -0
and +0
to manifest itself in your code:
-
It's obvious that negating
0
produces-0
. But the abstraction of an expression can cause-0
to creep in when you don't realize it. For example, consider:let stoppingForce = obj.mass * -obj.velocity
If
obj.velocity
is0
(or computes to0
), a-0
is introduced at that place and propogates out intostoppingForce
.
-
It's possible for a
-0
to be introduced into an expression as a return value of these methods in some cases, even when no-0
exists as one of the parameters. E.g., usingMath.pow
to raise-Infinity
to the power of any negative, odd exponent evaluates to-0
. Refer to the documentation for the individual methods.
-
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)
evalutes 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 on Object.is
when the signedness of zeroes 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.