Figuring Out the JavaScript Equality Operator
JavaScript can be weird. Anyone who has ever done any serious programming in that language has run up against behavior that had them scratching their head, or worse: slamming it against their desk because they just can’t find the source of that one weird bug. Something that can jerk around even the most seasoned programmer is the equality operator: ==
. The thing is, it’s really very simple if you just understand what goes on behind the scenes of that operation, especially where it comes to the core tenet of JavaScript: type conversion.
Many developers shun the ==
operator because they aren’t familiar with these rules, and they tend to knee-jerk rely on the strict ===
operator all the time. They think, “Well, dagnabbit, I’m used to type-safe languages, and that’s how things should be!” But type flexibility is a strength of JavaScript. Polymorphism is a core tenet of Object-Oriented Programming, and JavaScript has plenty of it. Blindly ignoring or even avoiding the strengths of a language prevents one from fully leveraging it. I encourage all JavaScript developers – including those who feel forced into using it – to simply learn these rules and Embrace the OOP.
Let’s look at some examples that we’ll explain one-by-one later. All these expressions evaluate to true:
null == undefined
1 == '.999999999999999999999999'
true == '1'
'42' == { valueOf: () => 42, toString:() => '67' }
'67' == { toString:() => '67' }
[1,2,3] == "1,2,3"
[] == 0
'[object Object]' == {}
And these evaluate to false:
true == 'true'
"[1,2,3]" == [1,2,3]
'67' == { valueOf: () => 42, toString:() => '67' }
new Date(1590466838174) == 1590466838174
({ valueOf: () => null }) == null
Type-Safety vs Type-Flexibility
It’s been my experience that the folks who have the most problems with this operator are those who are used to (and greatly prefer) type-safe languages like C/C#, and generally hold at least a little contempt for, shall we say, more type-flexible languages such as JavaScript. In type-safe languages, the equality-comparison operator (also ==
in C/C++/C#, by the way) is frequently reserved only for when the two operands are the same type to begin with. When one compares an integer to another integer, for example, we pretty much intrinsically understand what’s going to happen. The problem developers tend to have with JavaScript is that a variable can be any type at all: a number, a Boolean, a string, null, an arbitrary object, or even undefined. What happens when we try to compare a number with a string? Or with a Boolean? Well, good news: the JavaScript language specification says exactly what should happen. For those who learn the rules, it really is no mystery.
The Specification
There steps for evaluating x == y
are:
- If x and y are both the same type, just perform a simple strict comparison.
- If x is null and y is undefined (or vice-versa), return
true
. - If x is a string and y is a number, convert x to a number and try again.
- If x is a number and y is a string, convert y to a number and try again.
- If x is a Boolean, convert x to a number and try again (true becomes 1 and false becomes 0).
- If y is a Boolean, convert y to a number and try again.
- If x is a string, number or Symbol and y is an object, perform the ToPrimitive operation on y and try again.
- If x is an object and y is a string, number or Symbol, perform the ToPrimitive operation on x and try again.
- If we get here, return
false
The spec calls for the ToPrimitive operations in steps 7 and 8 to be called with no hint, so that simplifies the behavior of that operation for this specific use case. The ToPrimitive operation will first check to see if there is a valueOf
method on the object; if there is, it calls that method, and if the return value is an intrinsic primitive (number, string, or Boolean – anything other than an object), it returns that value. If there isn’t a valueOf
method or if it returns an object, ToPrimitive will simply return the results of calling the object’s toString
method. In this context, there is only one exception to this behavior: the ToPrimitive operation on a Date object will always call its toString
method, not valueOf
.
Therefore, the generalized rules for the == operator are:
- If the types are the same,
==
and===
behave identically: a simple equality operation. - null and undefined are always equal to each other.
- Comparing two intrinsic-typed values will perform a numeric conversion and comparison.
- Date objects will always perform a string comparison.
- Any other objects will compare with the results of its
valueOf
method if it has one and doesn’t return an object, otherwisetoString
.
Examples Explained
Now let’s look at the examples at the top of this article and walk through each one given these rules.
null == undefined
– null and undefined are always equal as per #21 == '.999999999999999999999999'
– x is a number and y is a string (#4), so we convert y to a number and compare. Depending on your browser and operating system, that value is likely to be represented as the integer 1, so 1 == 1 is true.true == '1'
– x is a Boolean (#5), so convert it to a number (true => 1, false => 0) and compare again. 1 == '1', which is back to #4, the string is converted to a number and 1 == 1 is true.'42' == { valueOf: () => 42, toString:() => '67' }
– x is a number and y is an object, which gets us to rule #7. The ToPrimitive operation is performed on the object. It has avalueOf
method that returns an integer value. The comparison then runs rule #3, converting the string “42” into the number 42, and 42 == 42.'67' == { toString:() => '67' }
– also rule #7, but this time the object doesn’t have avalueOf
method, so thetoString
result us used. That gives us the same-type comparison of strings, “67” == “67.”"1,2,3" == [1,2,3]
– x is a string and y is an object (an Array), so we hit #7 again and perform the ToPrimitive operation on the array. ThevalueOf
method of an array object returns the array itself, an object, so thetoString
method is called, performs a join on the array elements, and returns the string “1,2,3”. Since both sides are now strings, we’re back to rule #1 and “1,2,3” is equal to “1,2,3.”[] == 0
– because x is an object and y is a number, we trigger #8. ThevalueOf
method for an array returns the array, so we use thetoString
method. But there are no elements, sotoString
returns an empty string, sending us to #3 ("" == 0
). Converting an empty string to a number gives you a 0, so we compare 0 to 0 for a final answer of true. This would also work if comparing the empty array to an empty string or false ([] == false
and[] == ""
).'[object Object]' == {}
– also rule #7, but this time the object does not define either avalueOf
or atoString
method. However, all objects in JavaScript have an implicittoString
method, so this one is called. Most browsers will return the string “[object Object]”, so we are back to rule #1 with a same-type comparison of the strings “[object Object]” == “[object Object]”.true == 'true'
– this one is fun. Some developers may mistakenly believe that the==
operator converts differently typed operands to strings for its comparison, but that is not the case. Here x is a Boolean, so we hit rule #5, convert true to an integer value 1 and try again. Rule #4, where x is a number and y is a string, attempts to convert y to a number, but “true” is not a valid number format. The result is NaN, so we end up comparing 1 to NaN, which is false."[1,2,3]" == [1,2,3]
– like the previous similar example, the array object gets converted to a string because itsvalueOf
method does not return a primitive value. The error here is that the string it is compared to has square brackets around the elements, so the string comparison of “[1,2,3]” and “1,2,3” results in false.'67' == { valueOf: () => 42, toString:() => '67' }
– this is another example whereby developers who mistakenly believe operands are converted to strings for comparison may get tripped up. Because the object has avalueOf
method, its value is used, not the results oftoString
; 67 is not equal to 42.new Date(1590466838174) == 1590466838174
– this illustrates the exception to the “best to assume a numeric comparison” rule. The Date object’svalueOf
method does indeed return 1590466838174, but the Date object will always perform a==
comparison as a string. Depending on your locale, the actual string value of this Date object may vary, but rest assured it will not be “1590466838174.” I live in Honolulu, so for my Chrome browser, the string value of that Date is “Mon May 25 2020 18:20:38 GMT-1000 (Hawaii-Aleutian Standard Time).” Because y is a number, the date string gets converted to NaN, which is not equal to anything, let alone 1590466838174.({ valueOf: () => null }) == null
– trick question! Don’t get complacent and think that thevalueOf
method would get called on the object, it would return null, and null compared to null is true. Wrong! This expression does not trigger rule #8, because y is not a string, number, or Symbol. This expression falls all the way down through to rule #9 and returns false.
I hope that helps!