Primitive values vs. objects

When we talked about types in JS, we made a distinction between two different types in the language: primitive and objects types. We are now moving beyond the primitive to the realm of objects. But as you'll soon see, object types are fundamentally just structures that enable us to manage collection primitives.

Just to review, primitives include the following 5 types:

  • undefined
  • null
  • boolean
  • number
  • string

If you were to be asked, in an interview question, for example, how many types there are in JS, technically the answer is 6. The sixth primitive is one called a symbol. It's a newer feature to the language whose use is really limited to more advanced applications.

But what is important about the nature of primitive values is that whenever we make one ourselves, say we make the number 42:

const num = 42;

In this statement, 42 is created and when we assign it to a variable, in this case, or pass it to a function, their contents are copied. That is, the number 42 here is the number 42 anywhere.

If we were to create 42 in another variable, another num. If we were to see if num and anotherNum's contents were equal, what do you think we would get?

const num = 42;
const anotherNum = 42;
console.log(num === anotherNum);

We get true. Why? Because if primitives have the same value, they are recognized by JS as the same copied piece of data. Another way of saying this is that primitive values are passed by value.

This might seem obvious at first glance. Why wouldn't 42 be 42 everywhere? Or the string 'hello world':

console.log("hello world" === "hello world"); // true

But this observation matters because it's not necessarily the case with objects. As we mentioned at the beginning, objects are data structures for holding any number of primitives.

But as compared to primitives, if an object is assigned to a variable, it is not copied over, it's not passed by value. So let's create one now:

const obj = {};

When this empty object is assigned to this variable, what the variable receives is not a copy of it, unlike our primitive example, 42. Instead it gets what's known as a reference to it, not the exact object itself.

So if we were to create another obj, made exactly the same as the first:

const anotherObj = {};

And once again compare the two variables to see if their values are equal, what do you think we'll get:

console.log(obj === anotherObj);

We'll get false. And it doesn't even matter if we compare two empty objects directly, we'll still be told by JS that they aren't the same:

console.log({} === {});

So why is this? How could JS be telling us that every created object, even if it's exactly the same, is not equal to any other?

This is due to the nature of objects and what we can do with them. Unlike primitives, we have the ability to dynamically add properties to them. This is a necessary criteria of objects, which includes the 'subtypes' of objects, such as arrays, functions, maps and sets, basically all of the object types we'll be touching on at some point in this course. They are all objects.

But unlike objects, primitive values are immutable. Meaning you cannot change any of their properties. They have their own properties, but they can't be changed in any way. That means that any primitive value is not unique. It can't differ from any primitive that is the same data type and value. In other words, the boolean false can never change. We are always using the same value itself.

But because properties can be different in order to manage completely different sets of primitives, what happens when we make a new object is that we are making a totally unique value so to speak, unlike any other object, because it is stored in a completely different place in memory and is treated as different by the language due to it's ability to have its own custom properties. And therefore, whenever we pass an object to a variable or function, either one just holds a reference to it. In other words a pointer to it's unique place in memory. No matter how many times we pass an object to different variables or functions, it's always going to refer to the original created object.

So if we change our code, to take this obj and pass it to anotherObj:

const obj = {};
const anotherObj = obj;

If we update anotherObj with some property, no matter what it is:

anotherObj.a = 1;

What do you think we'll see when we console.log both obj and anotherObj? Will they be different? Will they be the same?

console.log("another obj", anotherObj); // another obj {a: 1}
console.log("obj", obj); // obj {a: 1}

For both variables, we have don't have two separate objects. We just have one shared reference. This is at the core of why this entire distinction matters: primitive vs reference types. Because of JS not passing a copy of the object, but just a reference, we updated both variables at once. This in most cases is not what we as developers want or expect.

So as a result need to 'fix' this default behavior of passing by reference that occurs with objects. In this section, we're going to learn how to make sure to update our objects without any surprises to make our code less error-prone and more reliable, but note that before we end this lesson that this nature of passing by reference is what gives objects their functionality. Ultimately, we wouldn't have the ability to dynamically add and update properties without objects being used by reference. So be mindful of the fact that there's a reason for this difference in behavior, it's not just a quirk of JS meant to trip you up.

So with that, let's move on to some practical examples of using objects hands-on.