Values and values equality comparison in JS
It’s crucial to understand what value types are there in JavaScript, and ways to compare them. Since we write a lot of conditional logic which relies on the fact if values are the same or not.
While it may seem like a pretty straightforward topic, there are many corner cases due to how the JavaScript engine works, and some cases are nonintuitive and hard to remember.
First, let’s make a quick recap of what value types are there.
Value types in JavaScript
There are 8 of them.
Value | Value type as returned by typeof operator |
---|---|
String | typeof string |
Number | typeof number |
BigInt | typeof bigint |
Boolean | typeof boolean |
Symbol | typeof symbol |
Null | typeof object |
Undefined | typeof undefined |
Object | typeof object |
Things to remember about this table.
All types are considered primitive types, except for an object
which is a more complex data type.
Number, besides the whole range of natural and float numbers, can also take the values of -Infinity
, +Infinity
, -0
, NaN
. All of the above are considered numbers.
There is a mistake in the logic of how typeof
operator works, because of which it shows null
as an object
. We have to remember that null
is a special, distinct value.
typeof(null) === 'object' // true (not actually true, it's null)
There is no function value type in JS. Functions are objects. The following code is just the way typeof
works.
typeof(() => {}) === 'function' //true (not actually true, but can come in handy)
Value equality comparison
There are a few ways we can check if the values are equal.
But first, we need to talk about what equality means for objects and functions (which are objects too, as we remember). Every time we create (declare) an object or a function, it’s a new one.
These objects are different. They are not equal.
const obj1 = {}
const obj2 = {}
Even if their ‘content’ is the same, they are still different objects.
const obj1 = {
prop: 'one'
}
const obj2 = {
prop: 'one'
}
Since they are not equal, and if we mutate the property of one object, another one will stay the same.
const obj1 = {
prop: 'one'
}
const obj2 = {
prop: 'one'
}
obj2.prop = 'two'
console.log(obj1.prop) // one
Loose equality a == b
and a != b
This is the most ambiguous way to compare values, as it does some unpredictable type conversions under the hood. First, it converts the types of values and after the conversion checks the values.
Here are some crazy examples.
"" == false // true
[] == "" // true
2 == "2" // true
true == 1 // true
false == [0] // true
It’s pretty much impossible to remember how it works in all those cases. We’re not going to dive into it. One thing to remember about the loose equality operator is not to use it.
Object.is(a, b)
This is the most predictable and easiest-to-remember way to compare values.
// different types
Object.is("", undefined) // false
Object.is(null, "string") //false
// same type, same value
Object.is('one', 'one') // true
Object.is(2, 2) // true
// same type, different values, we create new objects
Object.is(() => {}, () => {}) // false
Object.is({}, {}) // false
Now comes the tricky part, comparing all the numbers. The way I imagine it in my JavaScript head, number sequence looks like this.
-Infinity ... -n ... -2 -1 -0 NaN 0 1 2 ... +n ... +Infinity
Once I accept this, every value in the sequence becomes a unique, distinct value. And now, the following means perfect sense to me.
Object.is(-0, 0) // false
Object.is(NaN, 0) // false
Object.is(-0, +0) // false
Object.is(+Infinity, 0) // false
Object.is(NaN, NaN) // true
Strict equality a === b
and a !== b
This is the most popular equality comparison. And it’s great, we should totally use it. It doesn’t convert the types of values. So if the types differ it will return false
and won’t do the further comparison. It works pretty much the same as Object.is
but has a shorter notation, so it’s easier to type.
// different types
"" === undefined // false
null === "string" // false
// same type, same value
'one' === 'one' // true
2 === 2 // true
// same type, different values: we create new objects
(() => {}) === (() => {}) // false
{} === {} // false
The only difference with strict equality lies in how it handles the “special” numbers.
0 === -0 // true
NaN === NaN // false
If you think more about positive and negative zeros, it actually comes in handy that strict equality doesn’t make a difference. Because we humans don’t, too.
Consider the following example. We don’t think about the possibility of a negative zero appearing here.
const a = 5
const b = -5
const sum = a + b
const negativeSum = -sum
sum === negativeSum // true
While Object.is
will give us a wtf result.
Object.is(sum, negativeSum) // false
So, if strict equality works the same way as Object.is
, besides handling 0
and -0
comparison, which is so rare, that we can pretty much forget about it, that leaves us with only one corner case to remember.
Strict equality with NaN
We can’t use strict equality comparison to check if we somehow got a NaN
during our faulty math operations. And the latter can happen because all math operations in JS are “safe”. This means that we can do whatever weird operations we can, and our code will always run. We’ll just get a NaN
.
NaN + 5 // NaN
10 / NaN // NaN
"string" / 2 // NaN
Strict equality won’t work with NaN
.
if (value === NaN) {
// this will never happen
}
So we need to think of a different way to do an equality check for NaN
.
const value = NaN
Number.isNaN(value) // true
Object.is(value, NaN) // true
And there is one less obvious and somewhat confusing way to check for NaN
equality which exploits the very same strict equality operator peculiarity.
value !== NaN // true
Conclusion
This may be a confusing topic with lots of things to remember. But it all comes to the point that strict equality works in the most predictable way once we accept the “special” JavaScript number sequence.
-Infinity ... -n ... -2 -1 -0 NaN 0 1 2 ... +n ... +Infinity
After understanding this, the only thing we have to remember is that for checking equality with NaN
we have to use another way. Number.isNaN(value)
and Object.is(value, NaN)
will do.
Comments