== and ===
When I first applied to my coding bootcamp, one of the pre-interview quiz questions I was presented with was to "Explain the difference between ==
and ===
". I had no experience with a scripting language like JavaScript at the time, but I was allowed to Google my answer. From my research, the answer I came up with probably sounded something like, "===
compares whether two things are the same value and type, and ==
only compares whether two things are the same value." I've come to learn that this answer was incorrect. I don't know whether the admissions person that interviewed me would have been able to correct me or guide me to the correct answer, or if they even cared at all and were just trying to get someone to sign up and pay the tuition. At any rate, now that I've spent the last few blog posts developing a more firm grasp on how coercion in JavaScript works, it's time to explore the difference between ==
and ===
, and to really understand what makes the loose-equals comparison (==
) work.
==
and ===
are both comparison operators in JavaScript that compare two operands for equality. The operator with two equals signs, ==
, is commonly referred to as Loose Equals or Double Equals, and will perform a type coercion if the two operands are of different types. For example:
420 == "420";// expected result: true
The operator with three equals signs, ===
, is commonly referred to as Strict Equals or Triple Equals, and will not perform a type coercion if the two operands are of different types. For example:
420 === "420";// expected result: false
In both of these examples I am comparing a Number
, 420
, against a String
, "420"
. It's a common misconception that Loose Equals only checks the value of the two operands, and if that value is the same then it returns true
, and if those values aren't the same then it returns false
. What is really happening is that one (or both) of the values is getting coerced so that the two things getting compared are of the same type. After this coercion happens, the actual values can then be directly compared for equality. The jargony name for this operation is "Abstract Equality Comparison", and it is defined in Section 7.2.14 of ECMAScript language spec version 12.0.
In our first example, the first operand is a Number
and the second operand is a String
. So should the Number
get coerced to a String
or should the String
get coerced to a Number
? Section 7.2.14 details the algorithm to use to figure out which operand gets coerced (or whether both operands get coerced) and how. The relevant bit for the first example is:
If Type(
x
) isNumber
and Type(y
) isString
, return the result of the comparisonx == ! ToNumber(y)
.
As a bit of an aside, for our purposes here you can ignore the !
in that instruction above. Within the ES spec doc, for Abstract Operations the !
symbol is shorthand that relates to an abstract operation's completion record. This is well beyond the scope of this particular blog post. Suffice to say that in this case !
has nothing at all to do with the !
unary operator.
So setting aside the !
character, we're looking to find the result of x == ToNumber(y)
. So we'll rely on the ToNumber
Abstract Operation to coerce the String
"420"
to its Number
representation, which is 420
. Now that both operands are the same type, section 7.2.14 states: "If Type(x) is the same as Type(y), then [...] Return the result of performing Strict Equality Comparison x === y."
So at this point after the coercion operation, the first example is now 420 === 420
. The jargony specification term for ===
is "Strict Equality Comparison", and it is defined in Section 7.2.15 of the spec. The algorithm in this section states:
If Type(
x
) is different from Type(y
), returnfalse
.If Type(
x
) isNumber
orBigInt
, thena. Return
! Type(x)::equal(x, y)
.Return
! SameValueNonNumeric(x, y)
.
Essentially, since both operands are of the Number
type, the equality operation can just do an identity comparison. The Number
420
is indeed the same as the Number
420
, so the result returns true
. However, the first step in the Strict Equality Comparison algorithm is what makes our second example return false
, as the Number
420
and the String
"420"
are of different types.
In this way, Strict Equality Comparison (===
, otherwise known as Strict Equals or Triple Equals) does not permit type conversions in the equality comparison, but Abstract Equality Comparison (==
, otherwise known as Loose Equals or Double Equals) does. Neither is really more performant than the other, as both comparison operations check the type of both operands. Loose Equals simply defines steps to take when those types are different in order to make the operands into the same type so a direct Strict Equality Comparison can occur. Once that coercion has happened, then Abstract Equality Comparison and Strict Equality Comparison complete the same steps to produce either true
or false
.
Let's now take a look at the various type coercions as they happen for Abstract Equality Comparison.
Abstract Equality Comparison Type Coercions
It should come as no surprise that the very first thing that ==
does is check the types of both of its operands. If both operands are the same type, then it passes those operands onto ===
to do an identity check.
420 == 420;// expected result: true
"howdy" == "howdy";// expected result: true
var a = [1, 2, 3];a == a;// expected result: true
The exception here is NaN
, as NaN
is never equal to itself. It should come as no surprise for everything else that anything is equal to itself.
From spec:
If Type(
x
) is the same as Type(y
), thena. Return the result of performing Strict Equality Comparison
x === y
.
null
and undefined
Comparisons
The algorithm then checks whether its operands are null
and undefined
. In either case, it will return true
:
null == undefined;// expected result: true
undefined == null;// expected result: true
From spec:
- If
x
isnull
andy
isundefined
, returntrue
.- If
x
isundefined
andy
isnull
, returntrue
.
document.all
Comparisons
The next check is a weird one, defined in B.3.7.2. It pertains to a niche array-like Object
called document.all
, which is an old, nonstandard, deprecated part of legacy web browsers. The spec says that this very special Object
will only return true
when compared with null
, or undefined
, making it essentially a falsy Object
. If you've been paying attention to my last few blog posts, you'll note how strange this is as all other Object
s in JavaScript are truthy. The reason that this particular Object
is falsy is that coercing document.all
to a Boolean
was a method used to determine whether the code was running in an old, nonstandard version of Internet Explorer.
document.all == null;// expected result: true
document.all == undefined;// expected result: true
From spec:
- NOTE: This step is replaced in section B.3.7.2.
String
and Number
Comparisons
We've seen the next step already. If the first operand is a Number
and the second operand is a String
, then the spec says we need to coerce the String
to a Number
using the ToNumber
Abstract Operation.
420 == "420";// expected result: true
If the first operand is a String
and the second is a Number
, then the spec says to coerce the first operand to a Number
using the ToNumber
Abstract Operation.
"420" == 420;// expected result: true
From spec:
- If Type(
x
) is Number and Type(y
) isString
, return the result of the comparisonx == ! ToNumber(y)
.- If Type(
x
) isString
and Type(y
) isNumber
, return the result of the comparison! ToNumber(x) == y
.
String
and BigInt
Comparisons
If the first operand is a BigInt
and the second is a String
, then the spec states that the String
is coerced to a BigInt
representation.
var bigInt = 4205485488564835575723945398498489398434434;var str = "4205485488564835575723945398498489398434434";
bigInt == str;// expected result: true
If, however, the coercion of the String
to a BigInt
results in NaN
, then it returns false:
var bigInt = 4205485488564835575723945398498489398434434;var str = "42054yooooooooooooooooooooo8498489398434434";
bigInt == str;// expected result: false
If it's the second operand that's the String
and the first operand that's the BigInt
, then this same set of instructions is called recursively, but with the second operand as the first argument and the first operand as the second argument.
From spec:
If Type(
x
) isBigInt
and Type(y
) isString
, thena. Let
n
be! StringToBigInt(y)
.b. If
n
isNaN
, returnfalse
.c. Return the result of the comparison
x == n
.If Type(
x
) isString
and Type(y
) isBigInt
, return the result of the comparisony == x
.
Comparing Anything to a Boolean
In the case where anything gets compared to a Boolean
, the Boolean
value gets coerced to a Number
, and then the result of that ToNumber
Abstract Operation gets compared to the other value.
true == 420;// expected result: false
true == "420";// expected result: false
true == 1;// expected result: true
true == "1";// expected result: true
true == [1];// expected result: true
false == [];// expected result: true
false == [0];// expected result: true
false == 0;// expected result: true
false == "";// expected result: true
The above operations are the same if the first operand is the non-Boolean
and the second operand is the Boolean
:
420 == true;// expected result: false
"420" == true;// expected result: false
1 == true;// expected result: true
"1" == true;// expected result: true
[1] == true;// expected result: true
[] == false;// expected result: true
[0] == false;// expected result: true
0 == false;// expected result: true
"" == false;// expected result: true
From spec:
- If Type(
x
) isBoolean
, return the result of the comparison! ToNumber(x) == y
.- If Type(
y
) isBoolean
, return the result of the comparisonx == ! ToNumber(y)
.
Comparing String
/Number
/BigInt
/Symbol
to Object
When comparing any non-Boolean
scalar primitive to an Object
, the Object
gets coerced to a primitive representation using all of the rules of the ToPrimitive
Abstract Operation. If you've read my last three blog posts then you'll be very familiar with those rules by now. All of the foibles of ToPrimitive
apply as in other coercion operations - checks for [Symbol.toPrimitive]
, .toString()
, and .valueOf()
may all occur.
var obj1 = { a: 100, b: "Joey Reyes rules", c: true, d: [1, 2, 3], valueOf() { return this.a; },};
var obj2 = { a: 100, b: "Joey Reyes rules", c: true, d: [1, 2, 3], toString() { return this.b; },};
var obj3 = { a: 100, b: "Joey Reyes rules", c: true, d: [1, 2, 3],};
100 == obj1;// expected result: true
"Joey Reyes rules" == obj2;// expected result: true
"[object Object]" == obj3;// expected result: true
Once again, this is the same whether the Object
is the first or second operand.
var obj1 = { a: 100, b: "Joey Reyes rules", c: true, d: [1, 2, 3], valueOf() { return this.a; },};
var obj2 = { a: 100, b: "Joey Reyes rules", c: true, d: [1, 2, 3], toString() { return this.b; },};
var obj3 = { a: 100, b: "Joey Reyes rules", c: true, d: [1, 2, 3],};
obj1 == 100;// expected result: true
obj2 == "Joey Reyes rules";// expected result: true
obj3 == "[object Object]";// expected result: true
From spec:
- If Type(
x
) is eitherString
,Number
,BigInt
, orSymbol
and Type(y
) isObject
, return the result of the comparisonx == ? ToPrimitive(y)
.- If Type(
x
) isObject
and Type(y
) is eitherString
,Number
,BigInt
, orSymbol
, return the result of the comparison? ToPrimitive(x) == y
.
Number
and BigInt
Comparisons
If one of the operands is a Number
and the other is a BigInt
, then the algorithm first checks whether either is NaN
, +∞
, or -∞
. If that is the case then it returns false
, as these values cannot be equal to any other Number
or BigInt
. Otherwise, it checks whether the mathematical value of both numbers pass an identity check. I am a bit unclear on what the term mathematical value specifically means here, so I won't guess at it for the sake of coming up with an example, but I believe it relates to how these numbers are represented as real number values.
From spec:
If Type(x) is BigInt and Type(y) is Number, or if Type(x) is Number and Type(y) is BigInt, then
a. If x or y are any of NaN, +∞𝔽, or -∞𝔽, return false. b. If ℝ(x) = ℝ(y), return true; otherwise return false.
If All Else Fails...
If none of the above conditions are met for the two operands, then the algorithm simply returns false
.
From spec:
- Return
false
.
switch
Statements
I want to touch briefly on how switch
statements fit into this picture. A switch
statement is an alternate syntax for a chain of if...else
statements. For example:
switch (test) { case false: console.log("not true!"); break; case 420: console.log("coolest number"); break; default: console.log("nothing else applies");}
Essentially we have written a suite of test cases to compare test
to. Now let's define test
and see what happens:
var test = false;
switch (test) { case false: console.log("not true!"); break; case 420: console.log("coolest number"); break; default: console.log("nothing else applies");}
// expected result: "not true!"
If we set test
to the boolean false
, the condition for the first case is met, so the switch
statement logs "not true!"
, then hits the break;
statement, and stops running. Now let's change test
:
var test = true;
switch (test) { case false: console.log("not true!"); break; case 420: console.log("coolest number"); break; default: console.log("nothing else applies");}
// expected result: "nothing else applies"
If we set test
to the boolean true
, none of the conditions we define are met, so it falls back to the default
case, logs "nothing else applies"
and then stops running. Now let's look at the last case, which bring us back to the topic of ==
vs. ===
:
It might surprise you to learn that by default a switch
statement uses the Strict Equality algorithm for its matching. So in order to get case 420
's behavior to run, test
has to specifically be the number 420
.
var test = 420;
switch (test) { case false: console.log("not true!"); break; case 420: console.log("coolest number"); break; default: console.log("nothing else applies");}
// expected result: "coolest number"
If we set test
to the string "420"
, then it simply falls back to the default case:
var test = "420";
switch (test) { case false: console.log("not true!"); break; case 420: console.log("coolest number"); break; default: console.log("nothing else applies");}
// expected result: "nothing else applies"
However, we can adjust the switch
statement to sort of hack it into respecting the Loose Equality algorithm as follows:
switch (true) { case test == false: console.log("not true!"); break; case test == 420: console.log("coolest number"); break; default: console.log("nothing else applies");}
// expected result: "nothing else applies"
The initial argument passed to switch()
is simply the boolean true
, which ensures that each of the case
conditions run. The case
conditions will accept any expression, including a Loose Equals check, so the logic for Loose Equality can be put in place there. Now if we do the following we get the desired outcome:
var test = "420";
switch (true) { case test == false: console.log("not true!"); break; case test == 420: console.log("coolest number"); break; default: console.log("nothing else applies");}
// expected result: "coolest number"
Object.is()
ES6 added a static method called Object.is()
that enables us to make comparisons that are even more strict than ===
. Under the hood it is the exact same as the ===
algorithm, but exposes different behavior when comparing NaN
against NaN
and 0
against -0
:
With Strict Equals:
var nan = NaN;var zero = 0;var negZero = -0;
nan === nan; // expected result: falsezero === negZero; // expected result: true
With Object.is()
:
var nan = NaN;var zero = 0;var negZero = -0;
Object.is(nan, nan); // expected result: trueObject.is(zero, negZero); // expected result: false
Summary
==
and ===
are both comparison operators in JavaScript that compare two operands for equality. The operator with two equals signs, ==
, is commonly referred to as Loose Equals or Double Equals but its specification name is Abstract Equality Comparison. Depending on the types of the operands, ==
may perform a type coercion if the two operands are of different types. Section 7.2.14 of ECMAScript Spec Version 12.0 defines which operand gets coerced to which type in various situations, but at the end of the day they all fall back to the ToString
, ToNumber
, and ToPrimitive
Abstract Operations. The operator with three equals signs, ===
, is commonly referred to as Strict Equals or Triple Equals, but its specification name is Strict Equality Comparison. ===
will not perform a type coercion if the two operands are of different types, but otherwise checks both operands identity against one another.
switch
statements use Strict Equality by default, but Loose Equality can be used by hacking the switch
statement.
Object.is()
provides a way to check some of the items that even ===
permits, like NaN
not being equal to itself (for Object.is()
, one NaN
's value is equal to another NaN
's) or 0
and -0
(for Strict Equality, 0
and -0
are equal, but for Object.is()
they are distinct values).