Understanding Loose Equality In JavaScript
Tags: loose, javascript, ecma
Published: 2021-01-06 17:54:52

For those who prefer a more audiovisual form, a video almost identical to the article can be seen over here: 
{% youtube V3vZSO4zpFs %}

Abstract Equality, or as I've titled this article "Loose Equality" is (I think) one of the most misunderstood topics in JavaScript. People know loose equality, the double equals (`==`), to check if its operands are roughly equal to each other. The string `"55"` and the number `55` are *kind* of the same thing, but not *strictly* the same thing, with triple equals (`===`). 

People usually advise against using loose equality. Personally? Well if JavaScript came out with a ***strict** strict mode* that removed loose equality, I wouldn't be too bothered. 

But there's a lot of misinformation out there and I thought it would be helpful to clean some of that up. Which is why I've been working on this topic for so long.

```js
// loose equality vs strict equality
"55" == 55   // -> true
"55" === 55  // -> false
```

Loose equality, in reality, is a process that tries to *implicitly coerce* its operands to be the same type before passing it off to strict equal to give you the real result. *Implicit coercion* by itself actually isn't too bad. It's used in many other languages and JavaScript programmers use it pretty often.

In this example, we take advantage of [falsy and truthy values](https://developer.mozilla.org/en-US/docs/Glossary/Falsy) to check whether we should print out an array to the console. If the array exists and has a length property greater than 0, print it out.

```js
// example of implicit coercion
const myArr = [1, 2, 3, 4, 5];
if (myArr && myArr.length) {
  console.log("My arr is: " + myArr);
}
```

[Falsy](https://developer.mozilla.org/en-US/docs/Glossary/Falsy) values include all the JavaScript values that will evaluate to `false` when converted into a boolean.

```js
Boolean('')         // -> false
Boolean(0)          // -> false
Boolean(0n)         // -> false
Boolean(NaN)        // -> false
Boolean(null)       // -> false
Boolean(undefined)  // -> false
```

Do not confuse this with abstract equality, however. Double equals often does **not** rely on this system whatsoever. While using the exact same values, we get true for half only. I'm no statistician, but 50-50 looks like zero correlation to me.  

```js
false == ''         // -> true
false == 0          // -> true
false == 0n         // -> true
false == NaN        // -> false
false == null       // -> false
false == undefined  // -> false
```

In fact, I would go so far as to say the concept of falsy values **never** comes up within [abstract equality in the spec](https://www.ecma-international.org/ecma-262/11.0/index.html#sec-abstract-equality-comparison)? What's the spec?

The JavaScript specification is an esoteric document that instructs browsers on how JavaScript should work. Browsers all can code up the implementation themselves, but if you want to know how JavaScript works without digging through C++ code, this is the best place to look.

The spec can often be pretty confusing, but [this particular section](https://www.ecma-international.org/ecma-262/11.0/index.html#sec-abstract-equality-comparison) is actually kind of readable. It defines abstract equality as a list of steps and I think it's pretty cool. If you're ever wondering why null is loosely equal to undefined, this is why. Because it says so. There is no low-level reason why it must be that way - the discussion stops here. It works that way because the document says it should. 

While I can go through the document, I'm going to instead use a tool I've been working on to explain it a bit more simply - [The Abstract Equality Stepper](https://emnudge.dev/abs-eq/). I've written up the steps to roughly match spec. There are some minor changes in formatting to help with how my tool works, but it's essentially the same.

![preview of tool](https://dev-to-uploads.s3.amazonaws.com/i/ei0a2ba0nj8mfo6feycb.png)
 
Let's punch in some examples we've just shown to explore how this works. `false` and `0` perhaps.

![Untitled (1)](https://dev-to-uploads.s3.amazonaws.com/i/bb3jewk7iol0unfmp88z.png)

([View it here](https://emnudge.dev/abs-eq/#%22false%22,%220%22))

We can see that it declares either of the operands are a boolean, we convert the boolean to a number. Always. No matter what the other value is. 

Notice that it tells us to perform an abstract equality comparison, but these are the steps that **define** what an abstract equality comparison **is**. That's right, this is recursion. We restart with new values. Since the types are now equal, we chuck it off to a strict equality comparison which returns true since they're the same value.

Notice that abstract equality **uses** strict equality. 
![Untitled (2)](https://dev-to-uploads.s3.amazonaws.com/i/znygxq5csiw3tc1fyyz2.png)

So technically abstract equality must be less performant if the implementation matches the spec exactly. This is way too minor to matter in practice, but I thought it was interesting. 

Let's try `false` and `''`. We convert the boolean to a number like last time, but now we're left with a number versus a string.
 
![Untitled (3)](https://dev-to-uploads.s3.amazonaws.com/i/hf0n4oomm2nq8ae643p8.png)
 
([View it here](http://localhost:5000/#%22false%22,%22''%22))

We convert the string to a number and then go to strict equality. We're converting to numbers a lot here. It's for good reason. Numbers can be thought of as the most primitive type. It's easy to compare number to number and it's essentially what we're doing when we compare anything else. Even when we compare using reference equality (like with 2 objects) we're comparing memory locations, which, as you might have guessed, are numbers. 

We can substitute `0` for false for all of the other examples.

```js
0 == NaN        // -> false
0 == null       // -> false
0 == undefined  // -> false
```

 `0` isn't `NaN` so that's gonna be false. And then there is no step to define `0` and `null` or `undefined`, so we get `false` by **default.** 

![Untitled (4)](https://dev-to-uploads.s3.amazonaws.com/i/1clwmxf45begn38pwg7n.png) 

Nothing to do with falsy values here. Just looking at steps and following the rules. 

With that out of the way, let's look at a common example of abstract equality weirdness - a real headscratcher. 

# WTFJS - The Headscratcher

```js
![] == []  // -> true
```

This **looks** paradoxical, but it actually makes sense. First, we convert the left array to a boolean. This **does** involve the concept of falsy, but we haven't touched abstract equality yet, just expression evaluation. Since arrays aren't falsy, we would get `true`, but we're using an exclamation mark, so we flip that and get `false`. 

```js
false == []
```

Since booleans always turn to numbers in this system, our operands are `0` and `[]`. Now what?

Well, now we find ourselves face to face with the magical [`ToPrimitive`](https://jsitor.com/5ktE3jQzt8). This one is interesting. We can't just compare a primitive value and an object, we need 2 primitive values or 2 objects. We try turning our array into a primitive and out pops an empty string.

(Note: a function is just a callable object. When we use the term `object`, we include functions)

`0` and `''` means we turn the string into a number, which leads us to `0` and `0` which are equal.

But how does `ToPrimitive` work? What does it do?

We can [look at the spec again](https://www.ecma-international.org/ecma-262/11.0/index.html#sec-toprimitive), but this time it's a little more difficult, so I've taken the liberty of converting it to plain JavaScript.

{% jsitor 5ktE3jQzt8 %}

If we're passed a primitive value, just return that. No need to convert a primitive to a primitive. 

Then we check for a `Symbol.toPrimitive` property. This is a rather recent addition to JavaScript which allows you to define the `ToPrimitive` behavior a bit more easily.  
![Untitled (4)](https://dev-to-uploads.s3.amazonaws.com/i/9gvf9uq42aajigf4c147.png)
 
If such a method exists, we try to convert it to a number. How? We check for a `.valueOf` property, which is what `Number` calls. If you try adding your object to a number, it will try to look for this property and call it.

If this property doesn't exist on your object or it itself returns an object, we try converting it to a string. Using, of course, the `.toString` property. This is actually defined on all object by default, including arrays. If you don't touch your object then `ToPrimitive` will return a string. For arrays, this means returning all its values as a comma-separated list. If it's empty, that's an empty string.

```js
const obj = {
	valueOf() {
		console.log('calling valueOf');
		return 100;
	},
	toString() {
		console.log('calling toString');
		return '👀';
	}
};

console.log(obj + 43);

console.log(`I see you ${obj}`);
```

([Note: string concatenation itself doesn't always call `.toString`](https://twitter.com/emnudge/status/1345079883026857984))

And there's your explanation! 

But if you look a bit closer, you'll notice a few errors being thrown. Wait, does that mean...

Yup! There are often times where just using double equals will throw an error instead of returning false. Let's create such a scenario right now. 

# Throwing Errors With Equality Checks

```js
const obj1 = {
	[Symbol.toPrimitive]: 45
};

console.log(obj1 == 45);
// Uncaught TypeError: number 45 is not a function
```

We can also just make it a function, but return an object.

```js
const obj2 = {
	[Symbol.toPrimitive]: () => Object()
};

console.log(obj2 == 45);
// Uncaught TypeError: Cannot convert object to primitive value
```

Or do the same with the other methods

```js
const obj3 = {
	toString: () => Object(),
	valueOf: () => Object()
};

console.log(obj3 == 45);
// Uncaught TypeError: Cannot convert object to primitive value
```

Now, we can't actually delete these methods on most objects. I mentioned earlier that all objects implement this by default. All objects of course inherit this method from the object prototype and we can't really delete that.

However, it's possible to make an object with no prototype using `Object.create(null)`. Since it has no prototype, it has no `valueOf()` and no `toString()` and thus it will throw an error if we compare it to a primitive. Magical!

```js
Object.create(null) == 45
// Uncaught TypeError: Cannot convert object to primitive value
```

With that detour, let's close with the essence of this article - how to understand loose equality. 

# Conclusion

When comparing 2 things of different types, it'll help to convert the more complex type to a simpler representation. If we can convert to a number, do that. If we're adding an object to the mix, get the primitive value and again try squeezing a number out of it. 

`null` and `undefined` are loosely equal and that's that. 

If we get something like `Symbol()` or we compare `null` or `undefined` with anything else by each other, we get `false` **by default**. `Symbol()` actually has a `.toString()` method, but it doesn't really matter. The spec says we get `false`, so we get `false`.

If we want to describe the steps in a bit of a simpler form, it looks something like this:

1. null equals undefined
2. Number(string) == number
3. BigInt(string) == bigint
4. Number(boolean) == anything
5. ToPrimitive(object) == anything
6. BigInt(number) == bigint
7. false

Stay curious!