🍉

Values and values equality comparison in JS

March 03, 2023

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

2022 — 2023 Made by Arbuznik with ♡️ and Gatsby