The Abstract Equality Comparison Operator, a.k.a. == in JavaScript is the so-called type-coercing equality check. It coerces types if necessary and can lead to some pretty weird results.
But we can make sense of all this, and you'll see that you can follow along.
There is an algorithm behind it
Whenever you use the == operator, there is actually an algorithm behind it that determines the result of the comparison. This algorithm has its place within the ECMA spec (the spec behind JavaScript) and can be found in chapter 7.2.15.
It's actually pretty lengthy and takes a lot of space, but it covers all possible inputs and provides a concise way to determine the output of any comparison. You can see an excerpt from it below which I processed a little so you can read it better.
The Algorithm
- If Type(
x
) is the same as Type(y
), then- If Type(
x
) is Number OR BigInt, then- If
x
isNaN
, returnfalse
. - If
y
isNaN
, returnfalse
. - If
x
has the same value asy
, returntrue
. - If
x
is+0
andy
is-0
, returntrue
. - If
x
is-0
andy
is+0
, returntrue
. - Return
false
.
- If
- If Type(
x
) is Undefined, returntrue
. - If Type(
x
) is Null, returntrue
. - If Type(
x
) is String, then- if
x
andy
are exactly the same sequence of characters (same length, same characters in sequence), then- return
true
.
- return
- return
false
.
- if
- If Type(
x
) is Boolean, then- If
x
istrue
andy
istrue
, then- return
true
.
- return
- if
x
isfalse
andy
isfalse
, then- return
true
.
- return
- return
false
.
- If
- If Type(
x
) is Symbol, then- If
x
andy
are both the same Symbol value, then- return
true
.
- return
- return
false
.
- If
- If
x
andy
are the same Object value, then- return
true
.
- return
- return
false
.
- If Type(
- If
x
isnull
andy
isundefined
, returntrue
. - If
x
isundefined
andy
isnull
, returntrue
. - If Type(
x
) is Number and Type(y
) is String, return the result of the comparison x == ToNumber(y). - If Type(
x
) is String and Type(y
) is Number, return the result of the comparison ToNumber(x) == y. - If Type(
x
) is BigInt and Type(y
) is String, then- Set
n
to StringToBigInt(y). - If
n
isNaN
, return false. - Return the result of the comparison
x == n.
- Set
- If Type(
x
) is String and Type(y
) is BigInt, return the result of the comparisony == x
. - If Type(
x
) is Boolean, return the result of the comparison ToNumber(x) == y. - If Type(
y
) is Boolean, return the result of the comparison x == ToNumber(y). - If Type(
x
) is either String, Number, BigInt, or Symbol and Type(y
) is Object, return the result of the comparison x == ToPrimitive(y). - If Type(
x
) is Object and Type(y
) is either String, Number, BigInt, or Symbol, return the result of the comparison ToPrimitive(x) == y. - If Type(
x
) is BigInt and Type(y
) is Number, OR if Type(x
) is Number and Type(y
) is BigInt, then- If
x
ory
are any ofNaN
,+INFINITY
, or-INFINITY
, returnfalse
. - If the mathematical value of
x
is equal to the mathematical value ofy
, then- return
true
.
- return
- return
false
.
- If
- Return
false
.
All of this may seem a little intimidating, but that's okay. You'll see that there is enough logic to it to be understandable. But before we go in, you first have to learn about some functions that are referenced within this algorithm.
Supporting Functions
Type(x)
This is not the typeof operator, but a runtime function that returns exactly the type of a value at hand.
Type(null) is actually Null, for example, and not object.
ToNumber
This is also a runtime function. It basically works the same as calling Number(x).
StringToBigInt
This is basically ToNumber with a few additions which we won't cover now.
ToPrimitive
This is the runtime function to convert any complex object into a primitive. There is once again a whole algorithm to it and it goes as follows.
- If Type(input) is Object, then
- If
preferredType
is not present, sethint
to"default"
- Else If
preferredType
is hint String, sethint
to"string"
- Else
- Set
hint
to"number"
- Set
- Set
exoticToPrim
toinput[Symbol.iterator]
- If
exoticToPrim
is notundefined
, then- Set
result
toexoticToPrim(hint)
- If Type(
result
) is not Object, returnresult
- Throw a
TypeError
exception
- Set
- If
hint
is"default"
, sethint
to"number"
- If
hint
is"string"
, then- Set
methodNames
to["toString", "valueOf"]
- Set
- Else
- Set
methodNames
to["valueOf", "toString"]
- Set
- For each
name
in listmethodNames
, in order, do- If
input[name]
exists (not undefined), then - Set
result
toinput[name]()
- If Type(
result
) is not Object, returnresult
- If
- Throw a
TypeError
exception
- If
- If
- Return
input
Making Sense Of It With Examples
You now have 'a lot of algorithm' at hand, but it may still be pretty difficult to actually apply that knowledge. This is where examples come in. They usually help a lot in understanding more complex things.
Example 1
Let's start with 1 == "1"
as a light entry.
1 == "1";
// =>
// Step 4 applies, x is Number and y is string.
// So it is evaluated as
1 == Number("1");
// Number("1") yields 1, so we're at
1 == 1;
// which is
true;
Example 2
Let's continue our examples with true == 0
.
true == 0;
// =>
// Step 9 applies, as x is a boolean.
// It is evaluated as
Number(true) == 0;
// Number(true) yields 1, so we're at
1 == 0;
// which is
false;
Example 3
Let's go one step further and use one object in our comparison, so we'll go with "" == []
.
"" == [];
// =>
// Step 10 applies, as x is a string and y is an object.
// [] needs to be converted into a primitive.
// [][Symbol.toPrimitive] is undefined so that doesn't help.
// The type hint is "default", and thus "number" is passed, according to the primitive
// conversion algorithm
// The methods now used are [valueOf, toString].
// [].valueOf() returns [] again, so once again no help.
// [].toString() yields "" which is a primitive, so the algorithm terminates successfully.
"" == "";
// according to step 1.4.1 yields
true;
Example 4
Let's try [] == ![]
now. It's one of the weirder comparisons which usually makes people raise at least an eyebrow.
[] == ![];
// ![] is evaluated first and is no actual part of the comparison.
// ![] is a boolean conversion together with negation, so you could also
// read it as
[] == !Boolean([]);
// Boolean([]) yields true, as all objects do
[] == !true;
// and that negated is of course
[] == false;
// Now step 9 of the algorithm applies, as y is a boolean
[] == Number(false);
// Number(false) yields 0
[] == 0;
// And the algorithm calls itself again where this time, step 11 applies, as x is an object and y is a number
ToPrimitive([]) == 0;
// [][Symbol.toPrimitive] is undefined so that doesn't help.
// The type hint is still at "default" so it gets replaced with "number" according to step 1.6 of the primitive conversion algorithm.
// The methods used are now [valueOf, toString].
// [].valueOf() yields [] again which doesn't help.
// [].toString() however results in "", an empty string.
"" == 0;
// The algorithm is once again calling itself where this time step 5 applies, as
// x is a string and y is a number.
Number("") == 0;
// Number("") results in 0 and the algorithm calls itself once again.
0 == 0;
// This time step 1.1.3 applies, as both values have type number.
// Because both sides have the exact same value, the result can only be.
true;
What's next?
You can come up with your own examples now. Simply follow the algorithms and you'll surely get the right answer every time. But you can of course ensure that you are right by simply logging the expression in your browser's dev tool and then compare your result against what your browser says is right.
With a little more practice, you'll surely always know the answer to some JavaScript quizzes in the future!
Before you leave
If you like my content, visit me on Twitter, and perhaps you’ll like what you see.