Comparisons

Making decisions in programs requires comparing values to determine their identity and relationship to each other. JS has several mechanisms to enable value comparison, so let’s take a closer look at them.

Equal…ish

The most common comparison in JS programs asks the question, “Is this X value the same as that Y value?” What exactly does “the same as” really mean to JS, though?

For ergonomic and historical reasons, the meaning is more complicated than the obvious exact identity sort of matching. Sometimes an equality comparison intends exact matching, but other times the desired comparison is a bit broader, allowing closely similar or interchangeable matching. In other words, we must be aware of the nuanced differences between an equality comparison and an equivalence comparison.

If you’ve spent any time working with and reading about JS, you’ve certainly seen the so-called “triple-equals” === operator, also described as the “strict equality” operator. That seems rather straightforward, right? Surely, “strict” means strict, as in narrow and exact.

Not exactly.

Yes, most values participating in an === equality comparison will fit with that exact same intuition. Consider some examples:

  1. 3 === 3.0; // true
  2. "yes" === "yes"; // true
  3. null === null; // true
  4. false === false; // true
  5. 42 === "42"; // false
  6. "hello" === "Hello"; // false
  7. true === 1; // false
  8. 0 === null; // false
  9. "" === null; // false
  10. null === undefined; // false
NOTE:
Another way ===‘s equality comparison is often described is, “checking both the value and the type”. In several of the examples we’ve looked at so far, like 42 === “42”, the type of both values (number, string, etc.) does seem to be the distinguishing factor. There’s more to it than that, though. All value comparisons in JS consider the type of the values being compared, not just the === operator. Specifically, === disallows any sort of type conversion (aka, “coercion”) in its comparison, where other JS comparisons do allow coercion.

But the === operator does have some nuance to it, a fact many JS developers gloss over, to their detriment. The === operator is designed to lie in two cases of special values: NaN and -0. Consider:

  1. NaN === NaN; // false
  2. 0 === -0; // true

In the case of NaN, the === operator lies and says that an occurrence of NaN is not equal to another NaN. In the case of -0 (yes, this is a real, distinct value you can use intentionally in your programs!), the === operator lies and says it’s equal to the regular 0 value.

Since the lying about such comparisons can be bothersome, it’s best to avoid using === for them. For NaN comparisons, use the Number.isNaN(..) utility, which does not lie. For -0 comparison, use the Object.is(..) utility, which also does not lie. Object.is(..) can also be used for non-lying NaN checks, if you prefer. Humorously, you could think of Object.is(..) as the “quadruple-equals” ====, the really-really-strict comparison!

There are deeper historical and technical reasons for these lies, but that doesn’t change the fact that === is not actually strictly exactly equal comparison, in the strictest sense.

The story gets even more complicated when we consider comparisons of object values (non-primitives). Consider:

  1. [ 1, 2, 3 ] === [ 1, 2, 3 ]; // false
  2. { a: 42 } === { a: 42 } // false
  3. (x => x * 2) === (x => x * 2) // false

What’s going on here?

It may seem reasonable to assume that an equality check considers the nature or contents of the value; after all, 42 === 42 considers the actual 42 value and compares it. But when it comes to objects, a content-aware comparison is generally referred to as “structural equality.”

JS does not define === as structural equality for object values. Instead, === uses identity equality for object values.

In JS, all object values are held by reference (see “Values vs References” in Appendix A), are assigned and passed by reference-copy, and to our current discussion, are compared by reference (identity) equality. Consider:

  1. var x = [ 1, 2, 3 ];
  2. // assignment is by reference-copy, so
  3. // y references the *same* array as x,
  4. // not another copy of it.
  5. var y = x;
  6. y === x; // true
  7. y === [ 1, 2, 3 ]; // false
  8. x === [ 1, 2, 3 ]; // false

In this snippet, y === x is true because both variables hold a reference to the same initial array. But the === [1,2,3] comparisons both fail because y and x, respectively, are being compared to new different arrays [1,2,3]. The array structure and contents don’t matter in this comparison, only the reference identity.

JS does not provide a mechanism for structural equality comparison of object values, only reference identity comparison. To do structural equality comparison, you’ll need to implement the checks yourself.

But beware, it’s more complicated than you’ll assume. For example, how might you determine if two function references are “structurally equivalent”? Even stringifying to compare their source code text wouldn’t take into account things like closure. JS doesn’t provide structural equality comparison because it’s almost intractable to handle all the corner cases!

Coercive Comparisons

Coercion means a value of one type being converted to its respective representation in another type (to whatever extent possible). As we’ll discuss in Chapter 4, coercion is a core pillar of the JS language, not some optional feature that can reasonably be avoided.

But where coercion meets comparison operators (like equality), confusion and frustration unfortunately crop up more often than not.

Few JS features draw more ire in the broader JS community than the == operator, generally referred to as the “loose equality” operator. The majority of all writing and public discourse on JS condemns this operator as poorly designed and dangerous/bug-ridden when used in JS programs. Even the creator of the language himself, Brendan Eich, has lamented how it was designed as a big mistake.

From what I can tell, most of this frustration comes from a pretty short list of confusing corner cases, but a deeper problem is the extremely widespread misconception that it performs its comparisons without considering the types of its compared values.

The == operator performs an equality comparison similarly to how the === performs it. In fact, both operators consider the type of the values being compared. And if the comparison is between the same value type, both == and === do exactly the same thing, no difference whatsoever.

If the value types being compared are different, the == differs from === in that it allows coercion before the comparison. In other words, they both want to compare values of like types, but == allows type conversions first, and once the types have been converted to be the same on both sides, then == does the same thing as ===. Instead of “loose equality,” the == operator should be described as “coercive equality.”

Consider:

  1. 42 == "42"; // true
  2. 1 == true; // true

In both comparisons, the value types are different, so the == causes the non-number values ("42" and true) to be converted to numbers (42 and 1, respectively) before the comparisons are made.

Just being aware of this nature of ==—that it prefers primitive numeric comparisons—helps you avoid most of the troublesome corner cases, such as staying away from a gotchas like "" == 0 or 0 == false.

You may be thinking, “Oh, well, I will just always avoid any coercive equality comparison (using === instead) to avoid those corner cases”! Eh, sorry, that’s not quite as likely as you would hope.

There’s a pretty good chance that you’ll use relational comparison operators like <, > (and even <= and >=).

Just like ==, these operators will perform as if they’re “strict” if the types being relationally compared already match, but they’ll allow coercion first (generally, to numbers) if the types differ.

Consider:

  1. var arr = [ "1", "10", "100", "1000" ];
  2. for (let i = 0; i < arr.length && arr[i] < 500; i++) {
  3. // will run 3 times
  4. }

The i < arr.length comparison is “safe” from coercion because i and arr.length are always numbers. The arr[i] < 500 invokes coercion, though, because the arr[i] values are all strings. Those comparisons thus become 1 < 500, 10 < 500, 100 < 500, and 1000 < 500. Since that fourth one is false, the loop stops after its third iteration.

These relational operators typically use numeric comparisons, except in the case where both values being compared are already strings; in this case, they use alphabetical (dictionary-like) comparison of the strings:

  1. var x = "10";
  2. var y = "9";
  3. x < y; // true, watch out!

There’s no way to get these relational operators to avoid coercion, other than to just never use mismatched types in the comparisons. That’s perhaps admirable as a goal, but it’s still pretty likely you’re going to run into a case where the types may differ.

The wiser approach is not to avoid coercive comparisons, but to embrace and learn their ins and outs.

Coercive comparisons crop up in other places in JS, such as conditionals (if, etc.), which we’ll revisit in Appendix A, “Coercive Conditional Comparison.”