Loose Equals vs. Strict Equals
Loose equals is the ==
operator, and strict equals is the ===
operator. Both operators are used for comparing two values for “equality,” but the “loose” vs. “strict” indicates a very important difference in behavior between the two, specifically in how they decide “equality.”
A very common misconception about these two operators is: “==
checks values for equality and ===
checks both values and types for equality.” While that sounds nice and reasonable, it’s inaccurate. Countless well-respected JavaScript books and blogs have said exactly that, but unfortunately they’re all wrong.
The correct description is: “==
allows coercion in the equality comparison and ===
disallows coercion.”
Equality Performance
Stop and think about the difference between the first (inaccurate) explanation and this second (accurate) one.
In the first explanation, it seems obvious that ===
is doing more work than ==
, because it has to also check the type. In the second explanation, ==
is the one doing more work because it has to follow through the steps of coercion if the types are different.
Don’t fall into the trap, as many have, of thinking this has anything to do with performance, though, as if ==
is going to be slower than ===
in any relevant way. While it’s measurable that coercion does take a little bit of processing time, it’s mere microseconds (yes, that’s millionths of a second!).
If you’re comparing two values of the same types, ==
and ===
use the identical algorithm, and so other than minor differences in engine implementation, they should do the same work.
If you’re comparing two values of different types, the performance isn’t the important factor. What you should be asking yourself is: when comparing these two values, do I want coercion or not?
If you want coercion, use ==
loose equality, but if you don’t want coercion, use ===
strict equality.
Note: The implication here then is that both ==
and ===
check the types of their operands. The difference is in how they respond if the types don’t match.
Abstract Equality
The ==
operator’s behavior is defined as “The Abstract Equality Comparison Algorithm” in section 11.9.3 of the ES5 spec. What’s listed there is a comprehensive but simple algorithm that explicitly states every possible combination of types, and how the coercions (if necessary) should happen for each combination.
Warning: When (implicit) coercion is maligned as being too complicated and too flawed to be a useful good part, it is these rules of “abstract equality” that are being condemned. Generally, they are said to be too complex and too unintuitive for developers to practically learn and use, and that they are prone more to causing bugs in JS programs than to enabling greater code readability. I believe this is a flawed premise — that you readers are competent developers who write (and read and understand!) algorithms (aka code) all day long. So, what follows is a plain exposition of the “abstract equality” in simple terms. But I implore you to also read the ES5 spec section 11.9.3. I think you’ll be surprised at just how reasonable it is.
Basically, the first clause (11.9.3.1) says, if the two values being compared are of the same type, they are simply and naturally compared via Identity as you’d expect. For example, 42
is only equal to 42
, and "abc"
is only equal to "abc"
.
Some minor exceptions to normal expectation to be aware of:
NaN
is never equal to itself (see Chapter 2)+0
and-0
are equal to each other (see Chapter 2)
The final provision in clause 11.9.3.1 is for ==
loose equality comparison with object
s (including function
s and array
s). Two such values are only equal if they are both references to the exact same value. No coercion occurs here.
Note: The ===
strict equality comparison is defined identically to 11.9.3.1, including the provision about two object
values. It’s a very little known fact that ==
and ===
behave identically in the case where two object
s are being compared!
The rest of the algorithm in 11.9.3 specifies that if you use ==
loose equality to compare two values of different types, one or both of the values will need to be implicitly coerced. This coercion happens so that both values eventually end up as the same type, which can then directly be compared for equality using simple value Identity.
Note: The !=
loose not-equality operation is defined exactly as you’d expect, in that it’s literally the ==
operation comparison performed in its entirety, then the negation of the result. The same goes for the !==
strict not-equality operation.
Comparing: string
s to number
s
To illustrate ==
coercion, let’s first build off the string
and number
examples earlier in this chapter:
var a = 42;
var b = "42";
a === b; // false
a == b; // true
As we’d expect, a === b
fails, because no coercion is allowed, and indeed the 42
and "42"
values are different.
However, the second comparison a == b
uses loose equality, which means that if the types happen to be different, the comparison algorithm will perform implicit coercion on one or both values.
But exactly what kind of coercion happens here? Does the a
value of 42
become a string
, or does the b
value of "42"
become a number
?
In the ES5 spec, clauses 11.9.3.4-5 say:
- 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.
Warning: The spec uses Number
and String
as the formal names for the types, while this book prefers number
and string
for the primitive types. Do not let the capitalization of Number
in the spec confuse you for the Number()
native function. For our purposes, the capitalization of the type name is irrelevant — they have basically the same meaning.
Clearly, the spec says the "42"
value is coerced to a number
for the comparison. The how of that coercion has already been covered earlier, specifically with the ToNumber
abstract operation. In this case, it’s quite obvious then that the resulting two 42
values are equal.
Comparing: anything to boolean
One of the biggest gotchas with the implicit coercion of ==
loose equality pops up when you try to compare a value directly to true
or false
.
Consider:
var a = "42";
var b = true;
a == b; // false
Wait, what happened here!? We know that "42"
is a truthy value (see earlier in this chapter). So, how come it’s not ==
loose equal to true
?
The reason is both simple and deceptively tricky. It’s so easy to misunderstand, many JS developers never pay close enough attention to fully grasp it.
Let’s again quote the spec, clauses 11.9.3.6-7:
- 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).
Let’s break that down. First:
var x = true;
var y = "42";
x == y; // false
The Type(x)
is indeed Boolean
, so it performs ToNumber(x)
, which coerces true
to 1
. Now, 1 == "42"
is evaluated. The types are still different, so (essentially recursively) we reconsult the algorithm, which just as above will coerce "42"
to 42
, and 1 == 42
is clearly false
.
Reverse it, and we still get the same outcome:
var x = "42";
var y = false;
x == y; // false
The Type(y)
is Boolean
this time, so ToNumber(y)
yields 0
. "42" == 0
recursively becomes 42 == 0
, which is of course false
.
In other words, the value "42"
is neither == true
nor == false
. At first, that statement might seem crazy. How can a value be neither truthy nor falsy?
But that’s the problem! You’re asking the wrong question, entirely. It’s not your fault, really. Your brain is tricking you.
"42"
is indeed truthy, but "42" == true
is not performing a boolean test/coercion at all, no matter what your brain says. "42"
is not being coerced to a boolean
(true
), but instead true
is being coerced to a 1
, and then "42"
is being coerced to 42
.
Whether we like it or not, ToBoolean
is not even involved here, so the truthiness or falsiness of "42"
is irrelevant to the ==
operation!
What is relevant is to understand how the ==
comparison algorithm behaves with all the different type combinations. As it regards a boolean
value on either side of the ==
, a boolean
always coerces to a number
first.
If that seems strange to you, you’re not alone. I personally would recommend to never, ever, under any circumstances, use == true
or == false
. Ever.
But remember, I’m only talking about ==
here. === true
and === false
wouldn’t allow the coercion, so they’re safe from this hidden ToNumber
coercion.
Consider:
var a = "42";
// bad (will fail!):
if (a == true) {
// ..
}
// also bad (will fail!):
if (a === true) {
// ..
}
// good enough (works implicitly):
if (a) {
// ..
}
// better (works explicitly):
if (!!a) {
// ..
}
// also great (works explicitly):
if (Boolean( a )) {
// ..
}
If you avoid ever using == true
or == false
(aka loose equality with boolean
s) in your code, you’ll never have to worry about this truthiness/falsiness mental gotcha.
Comparing: null
s to undefined
s
Another example of implicit coercion can be seen with ==
loose equality between null
and undefined
values. Yet again quoting the ES5 spec, clauses 11.9.3.2-3:
- If x is null and y is undefined, return true.
- If x is undefined and y is null, return true.
null
and undefined
, when compared with ==
loose equality, equate to (aka coerce to) each other (as well as themselves, obviously), and no other values in the entire language.
What this means is that null
and undefined
can be treated as indistinguishable for comparison purposes, if you use the ==
loose equality operator to allow their mutual implicit coercion.
var a = null;
var b;
a == b; // true
a == null; // true
b == null; // true
a == false; // false
b == false; // false
a == ""; // false
b == ""; // false
a == 0; // false
b == 0; // false
The coercion between null
and undefined
is safe and predictable, and no other values can give false positives in such a check. I recommend using this coercion to allow null
and undefined
to be indistinguishable and thus treated as the same value.
For example:
var a = doSomething();
if (a == null) {
// ..
}
The a == null
check will pass only if doSomething()
returns either null
or undefined
, and will fail with any other value, even other falsy values like 0
, false
, and ""
.
The explicit form of the check, which disallows any such coercion, is (I think) unnecessarily much uglier (and perhaps a tiny bit less performant!):
var a = doSomething();
if (a === undefined || a === null) {
// ..
}
In my opinion, the form a == null
is yet another example where implicit coercion improves code readability, but does so in a reliably safe way.
Comparing: object
s to non-object
s
If an object
/function
/array
is compared to a simple scalar primitive (string
, number
, or boolean
), the ES5 spec says in clauses 11.9.3.8-9:
- If Type(x) is either String or Number 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 or Number,return the result of the comparison ToPrimitive(x) == y.
Note: You may notice that these clauses only mention String
and Number
, but not Boolean
. That’s because, as quoted earlier, clauses 11.9.3.6-7 take care of coercing any Boolean
operand presented to a Number
first.
Consider:
var a = 42;
var b = [ 42 ];
a == b; // true
The [ 42 ]
value has its ToPrimitive
abstract operation called (see the “Abstract Value Operations” section earlier), which results in the "42"
value. From there, it’s just 42 == "42"
, which as we’ve already covered becomes 42 == 42
, so a
and b
are found to be coercively equal.
Tip: All the quirks of the ToPrimitive
abstract operation that we discussed earlier in this chapter (toString()
, valueOf()
) apply here as you’d expect. This can be quite useful if you have a complex data structure that you want to define a custom valueOf()
method on, to provide a simple value for equality comparison purposes.
In Chapter 3, we covered “unboxing,” where an object
wrapper around a primitive value (like from new String("abc")
, for instance) is unwrapped, and the underlying primitive value ("abc"
) is returned. This behavior is related to the ToPrimitive
coercion in the ==
algorithm:
var a = "abc";
var b = Object( a ); // same as `new String( a )`
a === b; // false
a == b; // true
a == b
is true
because b
is coerced (aka “unboxed,” unwrapped) via ToPrimitive
to its underlying "abc"
simple scalar primitive value, which is the same as the value in a
.
There are some values where this is not the case, though, because of other overriding rules in the ==
algorithm. Consider:
var a = null;
var b = Object( a ); // same as `Object()`
a == b; // false
var c = undefined;
var d = Object( c ); // same as `Object()`
c == d; // false
var e = NaN;
var f = Object( e ); // same as `new Number( e )`
e == f; // false
The null
and undefined
values cannot be boxed — they have no object wrapper equivalent — so Object(null)
is just like Object()
in that both just produce a normal object.
NaN
can be boxed to its Number
object wrapper equivalent, but when ==
causes an unboxing, the NaN == NaN
comparison fails because NaN
is never equal to itself (see Chapter 2).
Edge Cases
Now that we’ve thoroughly examined how the implicit coercion of ==
loose equality works (in both sensible and surprising ways), let’s try to call out the worst, craziest corner cases so we can see what we need to avoid to not get bitten with coercion bugs.
First, let’s examine how modifying the built-in native prototypes can produce crazy results:
A Number By Any Other Value Would…
Number.prototype.valueOf = function() {
return 3;
};
new Number( 2 ) == 3; // true
Warning: 2 == 3
would not have fallen into this trap, because neither 2
nor 3
would have invoked the built-in Number.prototype.valueOf()
method because both are already primitive number
values and can be compared directly. However, new Number(2)
must go through the ToPrimitive
coercion, and thus invoke valueOf()
.
Evil, huh? Of course it is. No one should ever do such a thing. The fact that you can do this is sometimes used as a criticism of coercion and ==
. But that’s misdirected frustration. JavaScript is not bad because you can do such things, a developer is bad if they do such things. Don’t fall into the “my programming language should protect me from myself” fallacy.
Next, let’s consider another tricky example, which takes the evil from the previous example to another level:
if (a == 2 && a == 3) {
// ..
}
You might think this would be impossible, because a
could never be equal to both 2
and 3
at the same time. But “at the same time” is inaccurate, since the first expression a == 2
happens strictly before a == 3
.
So, what if we make a.valueOf()
have side effects each time it’s called, such that the first time it returns 2
and the second time it’s called it returns 3
? Pretty easy:
var i = 2;
Number.prototype.valueOf = function() {
return i++;
};
var a = new Number( 42 );
if (a == 2 && a == 3) {
console.log( "Yep, this happened." );
}
Again, these are evil tricks. Don’t do them. But also don’t use them as complaints against coercion. Potential abuses of a mechanism are not sufficient evidence to condemn the mechanism. Just avoid these crazy tricks, and stick only with valid and proper usage of coercion.
False-y Comparisons
The most common complaint against implicit coercion in ==
comparisons comes from how falsy values behave surprisingly when compared to each other.
To illustrate, let’s look at a list of the corner-cases around falsy value comparisons, to see which ones are reasonable and which are troublesome:
"0" == null; // false
"0" == undefined; // false
"0" == false; // true -- UH OH!
"0" == NaN; // false
"0" == 0; // true
"0" == ""; // false
false == null; // false
false == undefined; // false
false == NaN; // false
false == 0; // true -- UH OH!
false == ""; // true -- UH OH!
false == []; // true -- UH OH!
false == {}; // false
"" == null; // false
"" == undefined; // false
"" == NaN; // false
"" == 0; // true -- UH OH!
"" == []; // true -- UH OH!
"" == {}; // false
0 == null; // false
0 == undefined; // false
0 == NaN; // false
0 == []; // true -- UH OH!
0 == {}; // false
In this list of 24 comparisons, 17 of them are quite reasonable and predictable. For example, we know that ""
and NaN
are not at all equatable values, and indeed they don’t coerce to be loose equals, whereas "0"
and 0
are reasonably equatable and do coerce as loose equals.
However, seven of the comparisons are marked with “UH OH!” because as false positives, they are much more likely gotchas that could trip you up. ""
and 0
are definitely distinctly different values, and it’s rare you’d want to treat them as equatable, so their mutual coercion is troublesome. Note that there aren’t any false negatives here.
The Crazy Ones
We don’t have to stop there, though. We can keep looking for even more troublesome coercions:
[] == ![]; // true
Oooo, that seems at a higher level of crazy, right!? Your brain may likely trick you that you’re comparing a truthy to a falsy value, so the true
result is surprising, as we know a value can never be truthy and falsy at the same time!
But that’s not what’s actually happening. Let’s break it down. What do we know about the !
unary operator? It explicitly coerces to a boolean
using the ToBoolean
rules (and it also flips the parity). So before [] == ![]
is even processed, it’s actually already translated to [] == false
. We already saw that form in our above list (false == []
), so its surprise result is not new to us.
How about other corner cases?
2 == [2]; // true
"" == [null]; // true
As we said earlier in our ToNumber
discussion, the right-hand side [2]
and [null]
values will go through a ToPrimitive
coercion so they can be more readily compared to the simple primitives (2
and ""
, respectively) on the left-hand side. Since the valueOf()
for array
values just returns the array
itself, coercion falls to stringifying the array
.
[2]
will become "2"
, which then is ToNumber
coerced to 2
for the right-hand side value in the first comparison. [null]
just straight becomes ""
.
So, 2 == 2
and "" == ""
are completely understandable.
If your instinct is to still dislike these results, your frustration is not actually with coercion like you probably think it is. It’s actually a complaint against the default array
values’ ToPrimitive
behavior of coercing to a string
value. More likely, you’d just wish that [2].toString()
didn’t return "2"
, or that [null].toString()
didn’t return ""
.
But what exactly should these string
coercions result in? I can’t really think of any other appropriate string
coercion of [2]
than "2"
, except perhaps "[2]"
— but that could be very strange in other contexts!
You could rightly make the case that since String(null)
becomes "null"
, then String([null])
should also become "null"
. That’s a reasonable assertion. So, that’s the real culprit.
Implicit coercion itself isn’t the evil here. Even an explicit coercion of [null]
to a string
results in ""
. What’s at odds is whether it’s sensible at all for array
values to stringify to the equivalent of their contents, and exactly how that happens. So, direct your frustration at the rules for String( [..] )
, because that’s where the craziness stems from. Perhaps there should be no stringification coercion of array
s at all? But that would have lots of other downsides in other parts of the language.
Another famously cited gotcha:
0 == "\n"; // true
As we discussed earlier with empty ""
, "\n"
(or " "
or any other whitespace combination) is coerced via ToNumber
, and the result is 0
. What other number
value would you expect whitespace to coerce to? Does it bother you that explicit Number(" ")
yields 0
?
Really the only other reasonable number
value that empty strings or whitespace strings could coerce to is the NaN
. But would that really be better? The comparison " " == NaN
would of course fail, but it’s unclear that we’d have really fixed any of the underlying concerns.
The chances that a real-world JS program fails because 0 == "\n"
are awfully rare, and such corner cases are easy to avoid.
Type conversions always have corner cases, in any language — nothing specific to coercion. The issues here are about second-guessing a certain set of corner cases (and perhaps rightly so!?), but that’s not a salient argument against the overall coercion mechanism.
Bottom line: almost any crazy coercion between normal values that you’re likely to run into (aside from intentionally tricky valueOf()
or toString()
hacks as earlier) will boil down to the short seven-item list of gotcha coercions we’ve identified above.
To contrast against these 24 likely suspects for coercion gotchas, consider another list like this:
42 == "43"; // false
"foo" == 42; // false
"true" == true; // false
42 == "42"; // true
"foo" == [ "foo" ]; // true
In these nonfalsy, noncorner cases (and there are literally an infinite number of comparisons we could put on this list), the coercion results are totally safe, reasonable, and explainable.
Sanity Check
OK, we’ve definitely found some crazy stuff when we’ve looked deeply into implicit coercion. No wonder that most developers claim coercion is evil and should be avoided, right!?
But let’s take a step back and do a sanity check.
By way of magnitude comparison, we have a list of seven troublesome gotcha coercions, but we have another list of (at least 17, but actually infinite) coercions that are totally sane and explainable.
If you’re looking for a textbook example of “throwing the baby out with the bathwater,” this is it: discarding the entirety of coercion (the infinitely large list of safe and useful behaviors) because of a list of literally just seven gotchas.
The more prudent reaction would be to ask, “how can I use the countless good parts of coercion, but avoid the few bad parts?”
Let’s look again at the bad list:
"0" == false; // true -- UH OH!
false == 0; // true -- UH OH!
false == ""; // true -- UH OH!
false == []; // true -- UH OH!
"" == 0; // true -- UH OH!
"" == []; // true -- UH OH!
0 == []; // true -- UH OH!
Four of the seven items on this list involve == false
comparison, which we said earlier you should always, always avoid. That’s a pretty easy rule to remember.
Now the list is down to three.
"" == 0; // true -- UH OH!
"" == []; // true -- UH OH!
0 == []; // true -- UH OH!
Are these reasonable coercions you’d do in a normal JavaScript program? Under what conditions would they really happen?
I don’t think it’s terribly likely that you’d literally use == []
in a boolean
test in your program, at least not if you know what you’re doing. You’d probably instead be doing == ""
or == 0
, like:
function doSomething(a) {
if (a == "") {
// ..
}
}
You’d have an oops if you accidentally called doSomething(0)
or doSomething([])
. Another scenario:
function doSomething(a,b) {
if (a == b) {
// ..
}
}
Again, this could break if you did something like doSomething("",0)
or doSomething([],"")
.
So, while the situations can exist where these coercions will bite you, and you’ll want to be careful around them, they’re probably not super common on the whole of your code base.
Safely Using Implicit Coercion
The most important advice I can give you: examine your program and reason about what values can show up on either side of an ==
comparison. To effectively avoid issues with such comparisons, here’s some heuristic rules to follow:
- If either side of the comparison can have
true
orfalse
values, don’t ever, EVER use==
. - If either side of the comparison can have
[]
,""
, or0
values, seriously consider not using==
.
In these scenarios, it’s almost certainly better to use ===
instead of ==
, to avoid unwanted coercion. Follow those two simple rules and pretty much all the coercion gotchas that could reasonably hurt you will effectively be avoided.
Being more explicit/verbose in these cases will save you from a lot of headaches.
The question of ==
vs. ===
is really appropriately framed as: should you allow coercion for a comparison or not?
There’s lots of cases where such coercion can be helpful, allowing you to more tersely express some comparison logic (like with null
and undefined
, for example).
In the overall scheme of things, there’s relatively few cases where implicit coercion is truly dangerous. But in those places, for safety sake, definitely use ===
.
Tip: Another place where coercion is guaranteed not to bite you is with the typeof
operator. typeof
is always going to return you one of seven strings (see Chapter 1), and none of them are the empty ""
string. As such, there’s no case where checking the type of some value is going to run afoul of implicit coercion. typeof x == "function"
is 100% as safe and reliable as typeof x === "function"
. Literally, the spec says the algorithm will be identical in this situation. So, don’t just blindly use ===
everywhere simply because that’s what your code tools tell you to do, or (worst of all) because you’ve been told in some book to not think about it. You own the quality of your code.
Is implicit coercion evil and dangerous? In a few cases, yes, but overwhelmingly, no.
Be a responsible and mature developer. Learn how to use the power of coercion (both explicit and implicit) effectively and safely. And teach those around you to do the same.
Here’s a handy table made by Alex Dorey (@dorey on GitHub) to visualize a variety of comparisons: