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 objects (including functions and arrays). 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 objects 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: strings to numbers

To illustrate == coercion, let’s first build off the string and number examples earlier in this chapter:

  1. var a = 42;
  2. var b = "42";
  3. a === b; // false
  4. 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:

  1. If Type(x) is Number and Type(y) is String,return the result of the comparison x == ToNumber(y).
  2. 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:

  1. var a = "42";
  2. var b = true;
  3. 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:

  1. If Type(x) is Boolean,return the result of the comparison ToNumber(x) == y.
  2. If Type(y) is Boolean,return the result of the comparison x == ToNumber(y).

Let’s break that down. First:

  1. var x = true;
  2. var y = "42";
  3. 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:

  1. var x = "42";
  2. var y = false;
  3. 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:

  1. var a = "42";
  2. // bad (will fail!):
  3. if (a == true) {
  4. // ..
  5. }
  6. // also bad (will fail!):
  7. if (a === true) {
  8. // ..
  9. }
  10. // good enough (works implicitly):
  11. if (a) {
  12. // ..
  13. }
  14. // better (works explicitly):
  15. if (!!a) {
  16. // ..
  17. }
  18. // also great (works explicitly):
  19. if (Boolean( a )) {
  20. // ..
  21. }

If you avoid ever using == true or == false (aka loose equality with booleans) in your code, you’ll never have to worry about this truthiness/falsiness mental gotcha.

Comparing: nulls to undefineds

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:

  1. If x is null and y is undefined, return true.
  2. 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.

  1. var a = null;
  2. var b;
  3. a == b; // true
  4. a == null; // true
  5. b == null; // true
  6. a == false; // false
  7. b == false; // false
  8. a == ""; // false
  9. b == ""; // false
  10. a == 0; // false
  11. 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:

  1. var a = doSomething();
  2. if (a == null) {
  3. // ..
  4. }

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!):

  1. var a = doSomething();
  2. if (a === undefined || a === null) {
  3. // ..
  4. }

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: objects to non-objects

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:

  1. If Type(x) is either String or Number and Type(y) is Object,return the result of the comparison x == ToPrimitive(y).
  2. 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:

  1. var a = 42;
  2. var b = [ 42 ];
  3. 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:

  1. var a = "abc";
  2. var b = Object( a ); // same as `new String( a )`
  3. a === b; // false
  4. 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:

  1. var a = null;
  2. var b = Object( a ); // same as `Object()`
  3. a == b; // false
  4. var c = undefined;
  5. var d = Object( c ); // same as `Object()`
  6. c == d; // false
  7. var e = NaN;
  8. var f = Object( e ); // same as `new Number( e )`
  9. 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…

  1. Number.prototype.valueOf = function() {
  2. return 3;
  3. };
  4. 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:

  1. if (a == 2 && a == 3) {
  2. // ..
  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:

  1. var i = 2;
  2. Number.prototype.valueOf = function() {
  3. return i++;
  4. };
  5. var a = new Number( 42 );
  6. if (a == 2 && a == 3) {
  7. console.log( "Yep, this happened." );
  8. }

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:

  1. "0" == null; // false
  2. "0" == undefined; // false
  3. "0" == false; // true -- UH OH!
  4. "0" == NaN; // false
  5. "0" == 0; // true
  6. "0" == ""; // false
  7. false == null; // false
  8. false == undefined; // false
  9. false == NaN; // false
  10. false == 0; // true -- UH OH!
  11. false == ""; // true -- UH OH!
  12. false == []; // true -- UH OH!
  13. false == {}; // false
  14. "" == null; // false
  15. "" == undefined; // false
  16. "" == NaN; // false
  17. "" == 0; // true -- UH OH!
  18. "" == []; // true -- UH OH!
  19. "" == {}; // false
  20. 0 == null; // false
  21. 0 == undefined; // false
  22. 0 == NaN; // false
  23. 0 == []; // true -- UH OH!
  24. 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:

  1. [] == ![]; // 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?

  1. 2 == [2]; // true
  2. "" == [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 arrays at all? But that would have lots of other downsides in other parts of the language.

Another famously cited gotcha:

  1. 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:

  1. 42 == "43"; // false
  2. "foo" == 42; // false
  3. "true" == true; // false
  4. 42 == "42"; // true
  5. "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:

  1. "0" == false; // true -- UH OH!
  2. false == 0; // true -- UH OH!
  3. false == ""; // true -- UH OH!
  4. false == []; // true -- UH OH!
  5. "" == 0; // true -- UH OH!
  6. "" == []; // true -- UH OH!
  7. 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.

  1. "" == 0; // true -- UH OH!
  2. "" == []; // true -- UH OH!
  3. 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:

  1. function doSomething(a) {
  2. if (a == "") {
  3. // ..
  4. }
  5. }

You’d have an oops if you accidentally called doSomething(0) or doSomething([]). Another scenario:

  1. function doSomething(a,b) {
  2. if (a == b) {
  3. // ..
  4. }
  5. }

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:

  1. If either side of the comparison can have true or false values, don’t ever, EVER use ==.
  2. If either side of the comparison can have [], "", or 0 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:

Loose Equals vs. Strict Equals - 图1

Source: https://github.com/dorey/JavaScript-Equality-Table