== and ===

When I first applied to my coding bootcamp, one of the pre-interview quiz questions I was presented with was to "Explain the difference between == and ===". I had no experience with a scripting language like JavaScript at the time, but I was allowed to Google my answer. From my research, the answer I came up with probably sounded something like, "=== compares whether two things are the same value and type, and == only compares whether two things are the same value." I've come to learn that this answer was incorrect. I don't know whether the admissions person that interviewed me would have been able to correct me or guide me to the correct answer, or if they even cared at all and were just trying to get someone to sign up and pay the tuition. At any rate, now that I've spent the last few blog posts developing a more firm grasp on how coercion in JavaScript works, it's time to explore the difference between == and ===, and to really understand what makes the loose-equals comparison (==) work.

== and === are both comparison operators in JavaScript that compare two operands for equality. The operator with two equals signs, ==, is commonly referred to as Loose Equals or Double Equals, and will perform a type coercion if the two operands are of different types. For example:

420 == "420";
// expected result: true

The operator with three equals signs, ===, is commonly referred to as Strict Equals or Triple Equals, and will not perform a type coercion if the two operands are of different types. For example:

420 === "420";
// expected result: false

In both of these examples I am comparing a Number, 420, against a String, "420". It's a common misconception that Loose Equals only checks the value of the two operands, and if that value is the same then it returns true, and if those values aren't the same then it returns false. What is really happening is that one (or both) of the values is getting coerced so that the two things getting compared are of the same type. After this coercion happens, the actual values can then be directly compared for equality. The jargony name for this operation is "Abstract Equality Comparison", and it is defined in Section 7.2.14 of ECMAScript language spec version 12.0.

In our first example, the first operand is a Number and the second operand is a String. So should the Number get coerced to a String or should the String get coerced to a Number? Section 7.2.14 details the algorithm to use to figure out which operand gets coerced (or whether both operands get coerced) and how. The relevant bit for the first example is:

If Type(x) is Number and Type(y) is String, return the result of the comparison x == ! ToNumber(y).

As a bit of an aside, for our purposes here you can ignore the ! in that instruction above. Within the ES spec doc, for Abstract Operations the ! symbol is shorthand that relates to an abstract operation's completion record. This is well beyond the scope of this particular blog post. Suffice to say that in this case ! has nothing at all to do with the ! unary operator.

So setting aside the ! character, we're looking to find the result of x == ToNumber(y). So we'll rely on the ToNumber Abstract Operation to coerce the String "420" to its Number representation, which is 420. Now that both operands are the same type, section 7.2.14 states: "If Type(x) is the same as Type(y), then [...] Return the result of performing Strict Equality Comparison x === y."

So at this point after the coercion operation, the first example is now 420 === 420. The jargony specification term for === is "Strict Equality Comparison", and it is defined in Section 7.2.15 of the spec. The algorithm in this section states:

  1. If Type(x) is different from Type(y), return false.

  2. If Type(x) is Number or BigInt, then

    a. Return ! Type(x)::equal(x, y).

  3. Return ! SameValueNonNumeric(x, y).

Essentially, since both operands are of the Number type, the equality operation can just do an identity comparison. The Number 420 is indeed the same as the Number 420, so the result returns true. However, the first step in the Strict Equality Comparison algorithm is what makes our second example return false, as the Number 420 and the String "420" are of different types.

In this way, Strict Equality Comparison (===, otherwise known as Strict Equals or Triple Equals) does not permit type conversions in the equality comparison, but Abstract Equality Comparison (==, otherwise known as Loose Equals or Double Equals) does. Neither is really more performant than the other, as both comparison operations check the type of both operands. Loose Equals simply defines steps to take when those types are different in order to make the operands into the same type so a direct Strict Equality Comparison can occur. Once that coercion has happened, then Abstract Equality Comparison and Strict Equality Comparison complete the same steps to produce either true or false.

Let's now take a look at the various type coercions as they happen for Abstract Equality Comparison.

Abstract Equality Comparison Type Coercions

It should come as no surprise that the very first thing that == does is check the types of both of its operands. If both operands are the same type, then it passes those operands onto === to do an identity check.

420 == 420;
// expected result: true

"howdy" == "howdy";
// expected result: true

var a = [1, 2, 3];
a == a;
// expected result: true

The exception here is NaN, as NaN is never equal to itself. It should come as no surprise for everything else that anything is equal to itself.

From spec:

  1. If Type(x) is the same as Type(y), then

    a. Return the result of performing Strict Equality Comparison x === y.

null and undefined Comparisons

The algorithm then checks whether its operands are null and undefined. In either case, it will return true:

null == undefined;
// expected result: true

undefined == null;
// expected result: true

From spec:

  1. If x is null and y is undefined, return true.
  2. If x is undefined and y is null, return true.

document.all Comparisons

The next check is a weird one, defined in B.3.7.2. It pertains to a niche array-like Object called document.all, which is an old, nonstandard, deprecated part of legacy web browsers. The spec says that this very special Object will only return true when compared with null, or undefined, making it essentially a falsy Object. If you've been paying attention to my last few blog posts, you'll note how strange this is as all other Objects in JavaScript are truthy. The reason that this particular Object is falsy is that coercing document.all to a Boolean was a method used to determine whether the code was running in an old, nonstandard version of Internet Explorer.

document.all == null;
// expected result: true

document.all == undefined;
// expected result: true

From spec:

  1. NOTE: This step is replaced in section B.3.7.2.

String and Number Comparisons

We've seen the next step already. If the first operand is a Number and the second operand is a String, then the spec says we need to coerce the String to a Number using the ToNumber Abstract Operation.

420 == "420";
// expected result: true

If the first operand is a String and the second is a Number, then the spec says to coerce the first operand to a Number using the ToNumber Abstract Operation.

"420" == 420;
// expected result: true

From spec:

  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.

String and BigInt Comparisons

If the first operand is a BigInt and the second is a String, then the spec states that the String is coerced to a BigInt representation.

var bigInt = 4205485488564835575723945398498489398434434;
var str = '4205485488564835575723945398498489398434434';

bigInt == str;
// expected result: true

If, however, the coercion of the String to a BigInt results in NaN, then it returns false:

var bigInt = 4205485488564835575723945398498489398434434;
var str = '42054yooooooooooooooooooooo8498489398434434';

bigInt == str;
// expected result: false

If it's the second operand that's the String and the first operand that's the BigInt, then this same set of instructions is called recursively, but with the second operand as the first argument and the first operand as the second argument.

From spec:

  1. If Type(x) is BigInt and Type(y) is String, then

    a. Let n be ! StringToBigInt(y).

    b. If n is NaN, return false.

    c. Return the result of the comparison x == n.

  2. If Type(x) is String and Type(y) is BigInt, return the result of the comparison y == x.

Comparing Anything to a Boolean

In the case where anything gets compared to a Boolean, the Boolean value gets coerced to a Number, and then the result of that ToNumber Abstract Operation gets compared to the other value.

true == 420;
// expected result: false

true == "420";
// expected result: false

true == 1;
// expected result: true

true == "1";
// expected result: true

true == [1];
// expected result: true

false == [];
// expected result: true

false == [0];
// expected result: true

false == 0;
// expected result: true

false == "";
// expected result: true

The above operations are the same if the first operand is the non-Boolean and the second operand is the Boolean:

420 == true;
// expected result: false

"420" == true;
// expected result: false

1 == true;
// expected result: true

"1" == true;
// expected result: true

[1] == true;
// expected result: true

[] == false;
// expected result: true

[0] == false;
// expected result: true

0 == false;
// expected result: true

"" == false;
// expected result: true

From spec:

  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).

Comparing String/Number/BigInt/Symbol to Object

When comparing any non-Boolean scalar primitive to an Object, the Object gets coerced to a primitive representation using all of the rules of the ToPrimitive Abstract Operation. If you've read my last three blog posts then you'll be very familiar with those rules by now. All of the foibles of ToPrimitive apply as in other coercion operations - checks for [Symbol.toPrimitive], .toString(), and .valueOf() may all occur.

var obj1 = {
  a: 100,
  b: "Joey Reyes rules",
  c: true,
  d: [1, 2, 3],
  valueOf() {
    return this.a;
  },
}

var obj2 = {
  a: 100,
  b: "Joey Reyes rules",
  c: true,
  d: [1, 2, 3],
  toString() {
    return this.b;
  },
}

var obj3 = {
  a: 100,
  b: "Joey Reyes rules",
  c: true,
  d: [1, 2, 3],
}

100 == obj1;
// expected result: true

"Joey Reyes rules" == obj2;
// expected result: true

"[object Object]" == obj3;
// expected result: true

Once again, this is the same whether the Object is the first or second operand.

var obj1 = {
  a: 100,
  b: "Joey Reyes rules",
  c: true,
  d: [1, 2, 3],
  valueOf() {
    return this.a;
  },
}

var obj2 = {
  a: 100,
  b: "Joey Reyes rules",
  c: true,
  d: [1, 2, 3],
  toString() {
    return this.b;
  },
}

var obj3 = {
  a: 100,
  b: "Joey Reyes rules",
  c: true,
  d: [1, 2, 3],
}

obj1 == 100;
// expected result: true

obj2 == "Joey Reyes rules";
// expected result: true

obj3 == "[object Object]";
// expected result: true

From spec:

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

Number and BigInt Comparisons

If one of the operands is a Number and the other is a BigInt, then the algorithm first checks whether either is NaN, +∞, or -∞. If that is the case then it returns false, as these values cannot be equal to any other Number or BigInt. Otherwise, it checks whether the mathematical value of both numbers pass an identity check. I am a bit unclear on what the term mathematical value specifically means here, so I won't guess at it for the sake of coming up with an example, but I believe it relates to how these numbers are represented as real number values.

From spec:

  1. If Type(x) is BigInt and Type(y) is Number, or if Type(x) is Number and Type(y) is BigInt, then

    a. If x or y are any of NaN, +∞𝔽, or -∞𝔽, return false. b. If ℝ(x) = ℝ(y), return true; otherwise return false.

If All Else Fails...

If none of the above conditions are met for the two operands, then the algorithm simply returns false.

From spec:

  1. Return false.

switch Statements

I want to touch briefly on how switch statements fit into this picture. A switch statement is an alternate syntax for a chain of if...else statements. For example:

switch(test) {
  case false:
    console.log("not true!");
    break;
  case 420:
    console.log("coolest number");
    break;
  default:
    console.log("nothing else applies");
}

Essentially we have written a suite of test cases to compare test to. Now let's define test and see what happens:

var test = false;

switch(test) {
  case false:
    console.log("not true!");
    break;
  case 420:
    console.log("coolest number");
    break;
  default:
    console.log("nothing else applies");
}

// expected result: "not true!"

If we set test to the boolean false, the condition for the first case is met, so the switch statement logs "not true!", then hits the break; statement, and stops running. Now let's change test:

var test = true;

switch(test) {
  case false:
    console.log("not true!");
    break;
  case 420:
    console.log("coolest number");
    break;
  default:
    console.log("nothing else applies");
}

// expected result: "nothing else applies"

If we set test to the boolean true, none of the conditions we define are met, so it falls back to the default case, logs "nothing else applies" and then stops running. Now let's look at the last case, which bring us back to the topic of == vs. ===:

It might surprise you to learn that by default a switch statement uses the Strict Equality algorithm for its matching. So in order to get case 420's behavior to run, test has to specifically be the number 420.

var test = 420;

switch(test) {
  case false:
    console.log("not true!");
    break;
  case 420:
    console.log("coolest number");
    break;
  default:
    console.log("nothing else applies");
}

// expected result: "coolest number"

If we set test to the string "420", then it simply falls back to the default case:

var test = "420";

switch(test) {
  case false:
    console.log("not true!");
    break;
  case 420:
    console.log("coolest number");
    break;
  default:
    console.log("nothing else applies");
}

// expected result: "nothing else applies"

However, we can adjust the switch statement to sort of hack it into respecting the Loose Equality algorithm as follows:

switch(true) {
  case test == false:
    console.log("not true!");
    break;
  case test == 420:
    console.log("coolest number");
    break;
  default:
    console.log("nothing else applies");
}

// expected result: "nothing else applies"

The initial argument passed to switch() is simply the boolean true, which ensures that each of the case conditions run. The case conditions will accept any expression, including a Loose Equals check, so the logic for Loose Equality can be put in place there. Now if we do the following we get the desired outcome:

var test = "420";

switch(true) {
  case test == false:
    console.log("not true!");
    break;
  case test == 420:
    console.log("coolest number");
    break;
  default:
    console.log("nothing else applies");
}

// expected result: "coolest number"

Object.is()

ES6 added a static method called Object.is() that enables us to make comparisons that are even more strict than ===. Under the hood it is the exact same as the === algorithm, but exposes different behavior when comparing NaN against NaN and 0 against -0:

With Strict Equals:

var nan = NaN;
var zero = 0;
var negZero = -0;

nan === nan; // expected result: false
zero === negZero; // expected result: true

With Object.is():

var nan = NaN;
var zero = 0;
var negZero = -0;

Object.is(nan, nan); // expected result: true
Object.is(zero, negZero); // expected result: false

Summary

== and === are both comparison operators in JavaScript that compare two operands for equality. The operator with two equals signs, ==, is commonly referred to as Loose Equals or Double Equals but its specification name is Abstract Equality Comparison. Depending on the types of the operands, == may perform a type coercion if the two operands are of different types. Section 7.2.14 of ECMAScript Spec Version 12.0 defines which operand gets coerced to which type in various situations, but at the end of the day they all fall back to the ToString, ToNumber, and ToPrimitive Abstract Operations. The operator with three equals signs, ===, is commonly referred to as Strict Equals or Triple Equals, but its specification name is Strict Equality Comparison. === will not perform a type coercion if the two operands are of different types, but otherwise checks both operands identity against one another.

switch statements use Strict Equality by default, but Loose Equality can be used by hacking the switch statement.

Object.is() provides a way to check some of the items that even === permits, like NaN not being equal to itself (for Object.is(), one NaN's value is equal to another NaN's) or 0 and -0 (for Strict Equality, 0 and -0 are equal, but for Object.is() they are distinct values).

Sources / Further Reading