Equality, object references, and conditions in JavaScript

Date

There’s a lot of JavaScript information online, but a topic that doesn’t get enough attention in my opinion is how equality works in JavaScript and how fully understanding it along with object references can make your life much easier.

Equality affects a large amount of code you write, and even a slight misunderstanding can lead to lots of unwanted side effects in your code, particularly when using libraries such as React, which make extensive use of equality checks to determine if components should be re-rendered or if certain functions should be called.

Operators

One of the odd things about JavaScript is that there are two equality-checking operators:

  • Equality operator: == (double equals)
  • Strict equality operator: === (triple equals)

Of course there’s also the single equals (=) assignment operator but I won’t be covering that in this post.

“Loose” equality

The (loose) equality operator (==) checks if values are “equal”, even going as far as coercing some types to check if a value is equal to another. Here’s what I mean:

"1" == 1     // true
"1.0" == 1   // true
1 == true    // true
0 == false   // true
[] == false  // true
![] == false // true
!{} == false // true

Based on theses examples, you might reasonably expect the following to evaluate to true, but they are in fact false:

"false" == false // false
{} == false      // false
{} == true       // false

So while an empty array is “equal” to false, an empty object ({}) is not1. Likewise, while a string number is “equal” to an actual number, a string boolean does not “equal” a boolean.

This behavior is maddening, and is (understandably) one of the reasons why some people dislike JavaScript. Fortunately there’s a simple solution to this problem: don’t use this operator.

Strict equality

The (more sane) alternative is the strict equality operator (===). This one behaves as you would expect, only evaluating to true if both value and type are the same. The lines from the first example that evaluated to true, except for the last two, are expectedly false when using strict equality checks:

"1" === 1     // false
"1.0" === 1   // false
1 === true    // false
0 === false   // false
[] === false  // false
![] === false // true
!{} === false // true

The last two lines are still true in this case because negating a value with ! always equals the opposite. So negating a “truthy” value in these cases are correctly equal to false when using a strict equality check (I’ll explain more on “truthy” values further down).

In many other programming languages, double equals (==) behaves more like JavaScript’s strict equality operator (triple equals). When people come from these other languages, it’s only natural to think double-equals behaves the same and it ends up causing unexpected issues. It’s also very easy to accidentally type two equals when you meant to type three (or vice-versa).

My recommendation: Only use the strict equality operator and have your linter enforce this rule in your codebase with an error. With the proper tools, this is a wart that can be covered up and (for the most part) safely ignored.

Object references

Although the strict equality operator behaves in the way you’d expect, it’s also important to know what the behavior should be when comparing two things in JavaScript. Take the following for example:

"hello world" === "hello world" // true

{ hello: "world" } === { hello: "world" } // false

On the first line, strings (and other “primitives” like numbers, booleans, undefined, null, and symbols) are compared by value and type as we expect from the strict equality operator. Since the two strings are the same (value) and are of the same type, it evaluates to true. Nothing surprising here.

However, for the next line, although both sides of the comparison appear to be the same, the comparison is evaluated to false. Why?

In JavaScript, for non-primitive types such as objects (which includes arrays, functions, and anything that’s not a primitive mentioned above), every new object is only a reference to an actual object. In the second line above, although the two sides have an identical structure, they both refer to distinct object references so they are not equal (values are not the same)2. In the real world, a set of identical twins may look the same, but they are separate individuals.

To drive the point home further, imagine each object having a unique identifier (ID). It might make more sense if you think of the strict equality operator as checking if the IDs are equal, not if objects “look” the same:

const objectA = { hello: "world" }; // ID: 1
const objectB = { hello: "world" }; // ID: 2

objectA === objectB; // false
// ID:1 === ID:2    // false

Here’s another example:

const objectA = { hello: "world" }; // ID: 1
const objectB = { hello: "world" }; // ID: 2

const objectC = objectA; // ID: 1
const objectD = objectB; // ID: 2

objectA === objectC; // true
// ID:1 === ID:1    // true

objectB === objectD; // true
// ID:2 === ID:2    // true

This is also why passing a primitive to a function does not modify the original, but doing the same with an object can:

const str = "hello";
const obj = { hello: "world" };

function modifyString(s) {
  s = "world";
}

function modifyObject(o) {
  o.hello = "universe";
}

// Original values:
console.log(str); // "hello"
console.log(obj); // { hello: "world" }

modifyString(str);
modifyObject(obj);

// Modified values:
console.log(str); // "hello" (unchanged!)
console.log(obj); // { hello: "universe" }

Because string is a primitive type, it is copied over to the function. Modifying it in the function does nothing to the original since the function is only working with a local copy of the original value that exists only within its own scope. The original value is unchanged.

However, when we call modifyObject(obj), it is copying a reference to the object (not the object itself), so the original object is changed (mutated) when it is modified within the function.

NOTE: These are just examples! Please don’t re-assign (shadow) function arguments and unless absolutely necessary, don’t mutate objects in functions. There’s likely a better way to do what you’re trying to accomplish, and your team (and future self!) will thank you for writing clean code.

Conditions and “truthy” statements

When it comes to conditions (if, else if, switch, while, ternary operator ?:), the block will be executed (pass) if a value is “truthy”. More specifically, if it is not any of the following:

  • false (boolean)
  • null
  • undefined
  • 0 (zero, including -0, 0.0 and 0n)
  • "" (empty string)
  • NaN

All of the above values are considered “falsey” even if they are not exactly equal to false3. So while conditions often behave like a strict equality check (which is always exactly true or false), it’s important to note that “truthy” and “falsey” do not always mean true and false specifically.

Conclusion

By now you should understand the difference between == (loose equality) and === (strict equality), why you shouldn’t use the former, how primitive values and object references work, and how conditions are evaluated in JavaScript. It’s very important to understand these concepts before writing any serious code in JavaScript (and by extension TypeScript).

I plan on going deeper on other topics that make use of this knowledge later on (such as React hooks), and will be referring back to this post in the future as needed.


  1. This is made even more confusing by the fact that typeof [] is "object" ↩︎

  2. The strict equality operator is behaving consistently here because both type and value are not the same (both must be true). ↩︎

  3. In other words, “falsey” can be considered to mean false or “no value”. ↩︎