Understanding equality in JavaScript

We have all been there… You compare two values using ==, receive a totally unexpected result and wonder WTF is going on?! - Just Equality == operator at its finest.

Equality == operator has the following syntax.

x == y

It takes two operands, x and y, converts them into the same type, compares them by value and returns a Boolean result.

Download more icon variants from https://tabler-icons.io/i/info-circle

Always use Strict Equality `===` operator. There is literally no benefit in using `==` instead of `===`, except if your goal is to introduce bugs to your codebase.

But you did not come here so I can tell you to start using ===. Just look at the code below. You want to know how it can possibly be explained, don't you? - And yes, you should know it. After all, you are working with JavaScript and this is a JavaScript feature.

1'' == '0' // Result: false
20 == '' // Result: true
30 == '0' // Result: true
4
5false == undefined // Result: false
6false == null // Result: false
7null == undefined // Result: true
8
9[] == 0 // Result: true
10[] == '0' // Result: false
11['a', 'b'] == 'a,b' // Result: true
12
13NaN == NaN // Result: false
14'\n\r\t' == 0 // Result: true

IsLooselyEqual algorithm

When you use ==, under the hood, this algorithm kicks in and gives the result of comparison. So, yes, here lies the cure for our pain.

This algorithm is described in ECMAScript specification and basically every JavaScript engine has to implement it as described.

It is common to represent algorithms with some kind of graph or flow chart, but since I am very bad at drawing, I present to you this magnificent pseudocode.

1Take two operands, `x` and `y`, as an input
2
3If type of `x` is the same as type of `y`
4 If type of `x` is `Undefined`
5 return `true`
6 If type of `x` is `Null`
7 return `true`
8
9 If type of `x` is `Number`
10 If `x` is `NaN`
11 return `false`
12 If `y` is `NaN`
13 return `false`
14 If `x` is the same as `y`
15 return `true`
16 If `x` is `-0` and `y` is `+0`
17 return `true`
18 if `x` is `+0` and `y` is `-0`
19 return `true`
20 return `false`
21
22 If the type of `x` is `String`
23 If `x` and `y` are exactly the same sequence of characters
24 return `true`
25 return `false`
26
27 If the type of `x` is `Boolean`
28 If `x` and `y` are both `true`
29 return `true`
30 If `x` and `y` are both `false`
31 return `true`
32 return `false`
33
34 If `x` and `y` refer to the same object
35 return `true`
36
37 return `false`
38
39If `x` is `null` and `y` is `undefined`
40 return `true`
41If `x` is `undefined` and `y` is `null`
42 return `true`.
43
44If type of `x` is `Number` and type of `y` is `String`
45 Convert `y` to `Number`
46 Return the result of the comparison `x == y`
47If type of `x` is `String` and type of `y` is `Number`
48 Convert `x` to `Number`
49 Return the result of the comparison `x == y`
50
51If type of `x` is `Boolean`
52 Convert `x` to `Number`
53 Return the result of the comparison `x == y`
54If type of `y` is `Boolean`
55 Convert `y` to `Number`
56 Return the result of the comparison `x == y`
57
58If type of `x` is either `String` or `Number` and type of `y` is `Object`
59 Convert `y` to primitive value
60 Return the result of the comparison `x == y`
61If type of `x` is `Object` and type of `y` is either `String` or `Number`
62 Convert `x` to primitive value
63 Return the result of the comparison `x == y`
64
65Return `false`
Download more icon variants from https://tabler-icons.io/i/alert-triangle

Keep in mind that some parts of the algorithm are missing in pseudocode representation. If you want to see detailed specifications, check it out on ECMAScript website.

Yes, I know, it's just a bunch of if statements and conversions. This conversion part is really what's important. You see, when types are the same, == behaves exactly the same as the === operator. Important difference is that == enforces coercion when types are different.

Coercion is a fancy JavaScript term used when we talk about converting from one value type to another, like from string to number. Coercion plays an important role in == comparison.

If you don't want to remember the whole algorithm, think about it in this way. If operands do not have the same type, they are converted to Number. When types are the same, operands are compared by value.

Object to primitive

One more important thing, before we focus on some examples, is conversion of the Object type values to primitive values.

Download more icon variants from https://tabler-icons.io/i/info-circle

In JavaScript, there are 7 primitive values: string, number, bigint, boolean, undefined, null and symbol.

Value with the type of Object can automatically be converted to primitive value by == operator. This conversion will happen when the type of one operand is Object while the other operand has one of primitive types.

When converting Object to primitive value, JavaScript will do a couple of things. It will first try to call the built-in prototype method valueOf. If valueOf does not return a primitive value, the built-in prototype method toString will be called next.

As almost anything in JavaScript this behavior has a couple of edge cases and it can be modified and overridden, but you don't have to worry about it for now.

Examples

Still remember that code sample from the beginning? Let's cover it line by line, now when we know how the algorithm works.

I will make one assumption here. Operand on the left side of == will be called x and the operand on the right side will be called y. With that out of the way, let's get started.

Keep in mind, if operands do not have the same type, == will enforce type conversion as long as operands have different types. Only then will operands be compared by value.

Sample 01

'' == '0' // Result: false

This one is pretty simple. Both operands have the same type, String. This means that operands will be compared by value. Since values are not the same, the result is false.

Look at IsLooselyEqual algorithm on line: 3, 22 - 24.

Sample 02

0 == '' // Result: true

Type of operand x is Number and type of operand y is String. Types are not the same and y will be converted to Number.

Empty string '' converted to Number, will be 0. Test it by running Number('') in your browser's console.

When conversion is completed operands will be compared by value. Since values are the same, 0 is indeed equal to 0, result will be true.

Look at IsLooselyEqual algorithm on line: 44 - 46.

Sample 03

0 == '0' // Result: true

Type of operand x is Number and the type of operand y is String. Operand y will be converted to Number.

When '0' is converted to Number, its value is going to be 0. Test it by running Number('0') in your browser's console.

When conversion is completed operands will be compared by value. Since values are the same, 0 is indeed equal to 0, result will be true.

Look at IsLooselyEqual algorithm on line: 44 - 46.

Sample 04

false == undefined // Result: false
false == null // Result: false
null == undefined // Result: true

I grouped these examples together since the reasoning for this behavior, in all of them, is pretty much the same.

According to the algorithm, undefined can only be equal to undefined or null. Same is for null, it can only be equal to null or undefined. Because of this, comparing them to false will always result in false.

Look at IsLooselyEqual algorithm on line: 3 - 7, 39 - 42, 65.

Sample 05

[] == 0 // Result: true

Operand x is the type of Object and the type of operand y is String. As we already know, before they can be compared by value, type conversion is going to happen. In this case, operand x will be converted to a primitive value. This means Object to primitive conversion will kick in.

So, let's see how the conversion of [] to primitive value looks like. Method valueOf is going to be called on [] directly. Test it by running [].valueOf() in your browser's console. The result of this operation will be [].

As you can see, we still have [] which is not a primitive value, so method toString() gets called. Test it by running [].toString() in your browser's console. Returned value is '' which is a primitive value. With this, conversion of Object to primitive value is finally completed.

But, look closely, operands still have different types, x is now '' which is type String and y is still 0 which is type Number. Conversion must continue.

Now, x will be converted to Number. We already know, from previous examples, that '' is going to be 0 after conversion. When conversion is completed operands will be compared by value. Since values are the same, 0 is indeed equal to 0, result will be true.

Look at IsLooselyEqual algorithm on line: 61 - 63.

Sample 06

[] == '0' // Result: false

Operand x is type of Object, but operand y is type of String. Operand x, with value [], will be converted to primitive value. We already know, from Sample 05, the result of this conversion is going to be empty string ''. After conversion, operand x will have value '' and type String.

Since both operands are now the same type, String, conversion is completed. When compared by value, '' is not the same as '0', so this comparison is false.

Look at IsLooselyEqual algorithm on line: 61 - 63.

Sample 07

['a', 'b'] == 'a,b' // Result: true

Operand x is type of Object and operand y is type of String. In the conversion step, Object will be converted to primitive value.

After conversion, operand x will have value 'a,b' and type String. Test it by running ['a', 'b'].toString() in your browser's console.

Both operands are now types of String. When compared by value, the result is true, since values are the same.

Look at IsLooselyEqual algorithm on line: 61 - 63.

Sample 08

NaN == NaN // Result: false

This one is quite strange and yet simple. Comparing any value with NaN is always going to result in false. This is the case even if NaN is compared to NaN, which seems quite weird.

Look at IsLooselyEqual algorithm on line: 3, 9 - 13.

Sample 09

'\n\r\t' == 0 // Result: true

Operands have different types, so conversion is required. Since the type of x is String it will be converted to Number. After conversion, '\n\r\t' will be 0. Now, when compared by values, 0 is the same as 0, so the result is true.

Look at IsLooselyEqual algorithm on line: 47 - 49.

If you are wondering how '\n\r\t', after conversion, can become 0 - it's because of how conversion to Number works.

There are some special Single Character Escape Sequences like \n (new line), \t (horizontal tab), \v (vertical tab), \r (carriage return) that are always going to be 0 when converted to Number. It does not matter in which order and how many of them are contained inside the same string value.

Conclusion

We all know JavaScript can be weird, but if you look a bit under the hood, things can become much clearer. The same thing is with the Equality == operator. On the surface it can be really strange, but when you know the algorithm behind it, things can become much easier to understand.

Either way, I strongly recommend always using the Strict Equality === operator. But don't get me wrong, it is still really important to know how == works. It is, after all, a JavaScript feature which is always going to be part of the language.

© 2023 Ramo Mujagic. Thanks for visiting.