/ javascript

JavaScript: How does the == operator work?

Most JavaScript developers have a good idea what the double equal sign AKA loose equality operator == does. But there's some strange quirks and misunderstandings about it and they cause people to shy away from using it. For example, [''] == false evaluates to true. What causes this?

Type checking?

Some misunderstand the == operator in context of the === AKA strict equality operator. Mistakenly, it is common to think the === operator checks for type while == operator does not. This thinking certainly works in some usage of it.

console.log('2' == 2); // true
console.log('true' == true); // true
console.log('hi' == 2); // false

But that definition breaks down for expressions like [] == false. What is the type of an empty array? You can use the typeof operator and see it is of type 'object'. If you followed the naive definition of == and === operators, you would say [] and false are different types so the expression is false! But..

typeof []; // 'object'
typeof false; // 'boolean'
[] == false; // true whoa!!!!

Clearly, something else is going on.

Abstract Equality Comparison Algorithm

The answer to this mystery is two-fold. First, the behavior of the == operator is defined within the JavaScript specification as the Abstract Equality Comparison algorithm. In the latest ECMAScript spec, version 2017, section 7.2.13, it states the following:

7.2.13 Abstract Equality Comparison
The comparison x == y, where x and y are values, produces true or false. Such a comparison is performed as follows:

  1. If Type(x) is the same as Type(y), then
    Return the result of performing Strict Equality Comparison x === y.
  2. If x is null and y is undefined, return true.
  3. If x is undefined and y is null, return true.
  4. If Type(x) is Number and Type(y) is String, return the result of the comparison x == ToNumber(y).
  5. If Type(x) is String and Type(y) is Number, return the result of the comparison ToNumber(x) == y.
  6. If Type(x) is Boolean, return the result of the comparison ToNumber(x) == y.
  7. If Type(y) is Boolean, return the result of the comparison x == ToNumber(y).
  8. If Type(x) is either String, Number, or Symbol and Type(y) is Object, return the result of the comparison x == ToPrimitive(y).
  9. If Type(x) is Object and Type(y) is either String, Number, or Symbol, return the result of the comparison ToPrimitive(x) == y.
  10. Return false.

The Type(), ToNumber(), and ToPrimitive() methods are "abstract methods" used by implementers (eg people who make JavaScript engines) to follow the spec (eg the algorithm for the == operator). Number, String, Boolean, Symbol, Object are the types of the operands of the operation and the same as JavaScript types.

Steps 1 through 5 are pretty straight-forward. But let's look at the other steps.. For example, step 6 and 7: if either operand is of type Boolean, then convert the operand to type using ToNumber(). What does the abstract method ToNumber() do?

JavaScript Coercion

The second piece of the puzzle is coercion. The abstract method ToNumber(arg) converts arg to an output of type Number (if you're in ECMAScript-spec land or number if you're in JavaScript land). To fully understand it, we should know the other abstract methods ToString(), ToPrimitive(), and ToBoolean() too.

Keep in mind these abstract methods exist behind the scenes under the covers of the JavaScript engine and are used to perform these coercions behind the scenes. You can refer to the spec's section 7 to find out exactly what they do. I'll give a brief layman programmer's summary.

ToString()

  1. undefined, null, true, false, 3, NaN all get converted to string type literally (eg 'undefined', '3', 'NaN')
  2. For objects like a regular JS object or an array, it calls the JavaScript-land Object.prototype.toString() method which in ECMAScript spec land calls the ToPrimitive() abstract method then recursively call the ToString() method on the primitive value again.

ToPrimitive()

For coercion to number primitive (default type to coerce to):

  1. Calls JavaScript-land Object.prototype.valueOf().
  2. If the output's still an object then go to next step.
  3. Calls JavaScript-land Object.prototype.toString().
  4. If the output's still an object then go to next step.
  5. Return TypeError exception (eg this can happen if coercing objects created from Object.create(null) or object's prototype [[prototype]] or __proto__ is null).

If coercing to string primitive, then switch 1 and 3.

ToBoolean()

  1. If it's in this list: undefined, null, false, +0 (same as 0), -0, NaN, '', then return false.
  2. If it's not on that list, then return true.

ToNumber()

  1. For non-primitives, first run the ToPrimitive() abstract method.
  2. Then follow this conversion chart:
    • '' -> 0
    • -0 -> 0
    • . -> NaN
    • 0xaf -> 175
    • false -> 0
    • true -> 1
    • null -> 0
    • undefined -> NaN

Putting It All Together

So what exactly happens with [] == false? At first pass over the Abstract Equality Comparison algorithm, it fulfills step 7:

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

So it runs [] == ToNumber(false). ToNumber(false) becomes 0 and the expression becomes [] == 0. This recursively calls the algorithm and fulfills step 9:

If Type(x) is Object and Type(y) is either String, Number, or Symbol, return the result of the comparison ToPrimitive(x) == y.

Here, it runs ToPrimitive([]) == 0. ToPrimitive([]) runs and it goes through the steps:

  1. [].valueOf() -> []
  2. [] is still an object so go to next step.
  3. [].toString() -> ''
  4. '' is not an object, so return that value.

The expression becomes '' == 0. This recursively calls the algorithm and fulfills step 5:

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

The expression becomes ToNumber('') == 0. ToNumber('') results in 0. It becomes 0 == 0 and recursively the algorithm is called again and fulfills the step 1:

If Type(x) is the same as Type(y), then
Return the result of performing Strict Equality Comparison x === y.

Finally we get 0 === 0 which is true!

Of course, it's not very practical to go through this algorithm everytime you find == in your code. So should we avoid using it? I like what Kyle Simpson suggests in his series You Don't Know JS:

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

Basically, use it if you need to and can safely use coercion. Personally, I can only count a handful of times I have deliberately used the == operator. Usually, it's from an old, improperly typed backend response that wasn't worth changing (eg JSON response with properties that were supposed to be of type number but got returned as string).

Yes.. coercion can be a blessing and a curse in JavaScript.