In this appendix, I present some of the problematic features of JavaScript that are easily avoided. By simply avoiding these features, you make JavaScript a better language, and yourself a better programmer: == (loose equality)
That’s how the great Douglas Crockford in his excellent Javascript: The Good Parts introduced the bad parts in javascript and listed the loose equality aka ==
operator as the first bad part of this programming language. It’s a very common advice for javascript newcomers to completely abandon ==
and use his twin ===
operator instead. There’s also an eslint rule eqeqeq to force developer completely get rid of ==
. What’s really ==
, how does it work under the hood and how is it different to their twin ===
? If those questions intrigue you, this post can give you the answer.
The quiz - magic number
To get started, this’s a warm-up challenge for you. The answer and explanation can be found at the end of this post.
Can you define the variable magicNumber
so that the string "magic"
is printed to the console?
const magicNumber = ?????
if (magicNumber == 1 && magicNumber == 2 && magicNumber == 3) {
console.log('magic');
}
Strict equality
Here’s the full definition of the strict equality comparison algorithm in the spec. To sum it up, ===
disallows coercion. If two values have different types, the comparison’s result is always false
.
1 === 1; // true
1 === '1'; // false
1 === true; // false
Loose equality
In contrast to ===
, ==
allows coercion in the equality comparison. Take a look at the loose equality comparison algorithm described in the specification. Let me summarize how the double equal works:
- If two values have the same type,
==
behaves exactly like===
- If both of them are
null
orundefined
, it returnstrue
- If either or both of them are not primitive, they will be converted to primitive first by
ToPrimitive
- If both of them are primitive, there are 2 possible scenarios:
- If they have the same type,
===
’s algorithm is applied. - If they have different types,
ToNumber
is preferred to convert them to the same type.
- If they have the same type,
Let’s examine an example to understand the algorithm better:
const num1 = 25;
const num2 = [25];
if (num1 == num2) {
console.log('They are equal');
}
// Output: They are equal
Here’s what took place:
[25]
is not primitive so it has to be converted to primitive (Take a look at this post to understand howToPrimitive
works), the result ofToPrimitive([25])
is"25"
- Both
25
and"25"
are primitive but the have different type,ToNumber
will come into play to convert"25"
to number and the result is the number25
- Because
25
is of course equals to25
, the stringThey are equal
is logged to the console.
Double equals corner cases
Because the value can be converted during the comparison, it will lead to some “weird” results if we don’t have a strong grip on type coercion. Let’s see some edge cases with double equal to understand why this feature gets so much hatred from developers
[] == ![]
is true
Let’s do the comparison step by step:
- both of values are not primitive so they will be converted to primitive.
ToPrimitive([])
returns""
, so this is equivalent to"" == !""
- The exclamation mark converts
""
to a boolean value, which isfalse
. So this’s equivalent to"" == false
- Both
""
andfalse
are primitive but have different types,false
will be converted to number first byToNumber
, which is0
- Then
""
is also converted to number, which is0
- The expression is equivalent to:
0 == 0
and returnstrue
Another edge case
const emptyArray = [];
if (emptyArray) {
console.log('yes');
}
if (emptyArray == true) {
console.log('đúng');
}
if (emptyArray == false) {
console.log('ja');
}
// Output:
// yes
// ja
- In the first
if
statement, we check ifemptyArray
is truthy value. According to the spec,[]
is not a falsy value, so it’s truthy. That’s why the string"yes"
is printed to console. - With the second
if
statement, we compare[]
withtrue
. Let’s do it step by step:[]
is not primitive, so it is converted to primitive, which is""
, and this is equivalent to"" == true
- Both of them are primitive but have different types,
true
is converted to number first, which returns1
- Next, the
""
is converted to number byToNumber
and the result is 0 - The expression is
0 == 1
and of course the string"đúng"
is not printed.
- The same logic happens as in case 2.
Cases for loose equality
After having some foundations of strict and loose equal, the next question is should we continue ignoring ==
and sticking to ===
? The ball is on your court.
Personally, the only case I use ==
is obj == null
for checking if the object is null
or undefined
. If both of 2 values is null
or undefined
, they are equal so only one ==
comparison is enough to check both cases. In comparison to ===
, the code is prettier and much more concise .
Kyle Simpson has a controversial opinion that the usage of ==
should be preferred and ===
should only be a last option. Dr. Axel Rauschmayer also wrote a blog post about possibles usage of ==
in javascript. Please checkout those resources, take the time and make a wise choice for yourself.
Magic number - solution
const magicNumber = {
value: 1,
valueOf() {
return this.value++;
},
};
if (magicNumber == 1 && magicNumber == 2 && magicNumber == 3) {
console.log('magic');
}
// Output: "magic"
There’s no number that can be equals to 1, 2 and 3 at the same time but we are using ==
for the comparison, that means type coercion is allowed. Let’s see what happens when comparing magicNumber == 1
magicNumber
is converted to primitive. The abstract operatorToPrimitive
is used to convertmagicNumber
to primitive. Because we are comparingmagicNumber
with a number, the hint “number” will be sent toToPrimitive
. Therefore,Object.prototype.valueOf()
will be executed firstly.- The
valueOf
ofmagicNumber
performs 2 things at the same time: it returns thevalue
property which is currently1
and incrementvalue
by 1. - Therefore, in the next comparison,
valueOf()
returns2
and incrementvalue
by 1. - The process repeats after and after and now we have a magic number that loosely equals to 1, 2 and 3