const - JS | lectureHow JavaScript Works Behind the Scenes

Primitives VS Objects in Practice

How JavaScript Works Behind the Scenes

Let's start by creating a similar example to what we had in the last lecture to see this happening in practice. And just like in the previous lecture, let's start by mutating a primitive value.

script.js
let lastName = 'Jones';
/**
 * Let's say this person gets married 💍
 * and decided to change their last name.
 */
let oldLastname = lastName;
lastName = 'Davis';

If we now log both of them to the console,

script.js
console.log(lastName)
console.log(oldLastName)

Primitives vs Objects - Practice - 1

We see that they are, in fact, different. Davis is the new last name, and Jones is this old last name that was copied on the second line of code. Everything works as we would expect in an intuitive way. Remember that it works this way because each primitive value will simply be saved into its own piece of memory in the stack.

Let's do the same thing with an object, which, as we already know, is a reference value because it is going to be stored in the heap, and the stack then just keeps a reference to the memory position at which the object is stored in the heap.

script.js
const melissa = {
  firstName: 'Melissa',
  lastName: 'Jones',
  age: 27,
};

const marriedMelissa = melissa;

On the above highlighted line, we are copying the entire object. At least that's what it looks like, but behind the scenes, we are actually just copying the reference, which will then point to the same object.

Now as we change the last name on marriedMelissa, we already know that this will not give us the result that we expect.

script.js
marriedMelissa.lastName = 'Davis';
console.log('Before marriage: ', melissa);
console.log('After marriage: ', marriedMelissa);

Primitives vs Objects - Practice - 2

We can see from the console that we get Davis as the last name before the marriage and after the marriage. At this point, we already know why this happened. If you don't remember why it happened, when we attempted to copy the original melissa object, it did not create a new object in the heap.

That is, marriedMelissa is not a new object in the heap. It's simply just another variable in the stack that holds the reference to the original object. So, both variables melissa, and marriedMelissa simply point to the same memory address in the heap. And that's because, in the stack, they both hold the same memory address reference which therefore makes sense that if we change a property on marriedMelissa, it will also change on melissa itself.

This is also the reason why we can change properties in the marriedMelissa object, which was declared using a const (Things that we cannot change). However, what actually needs to be constant is the value in the stack. In the stack, the value only holds the reference, which we are not actually changing. The only thing that we are changing is the underlying object that is stored in the heap. And that is okay to change, that has nothing to do with const or let, all right? That's only about the value in the stack, but if we change something in the heap that has nothing to do with const or let.

Now, what we can't do is to assign a completely different object to marriedMelissa. For example, the below code will not work.

script.js
marriedMelissa = {};

Primitives vs Objects - Practice - 3

It does not work because this new object will be stored at a different position in memory, and therefore the reference to that position in memory will have to change here in this variable. And therefore, that does not work. Because that is in the stack, and since it is a constant, we cannot change that value in the stack.

If marriedMelissa was declared with a let, then it woud have worked. But since it's a constant, again, it is not allowed. So, as a conclusion, completely changing the object, i.e., assigning a new object is completely different than simply changing a property. It's a fundamental difference. So, please make sure to really understand this.

What if we actually really wanted to copy the object so that we could then change one of them without changing the other? Let me show you a way in which we can do that.

Let's create a new object called leslie:

script.js
const leslie = {
  firstName: 'Leslie',
  lastName: 'Alexander',
  age: 24,
};

If we really wanted to copy this leslie object, we could use a function called Object.assign. What this function does is to essentially merge two objects and then return a new one.

script.js
// Merging an empty object with leslie

Object.assign({}, leslie);

Doing the above will then create a completely new object where all the properties are really copied. So, the result of calling this function with the provided arguments will be a new object.

script.js
const marriedLeslie = Object.assign({}, leslie);
marriedLeslie.lastName = 'Walton'
console.log('Before marriage: ', leslie);
console.log('After marriage: ', marriedLeslie);

Primitives vs Objects - Practice - 4

From the above result, we see that were able to preserve the original last name Alexander after we changed the last name on the marriedLeslie object. What this means is that marriedLeslie is indeed a real copy of the original. So, all the properties were essentially copied from one object to the other. And so, behind the scenes, what this means is that a new object was in fact created in the heap, and marriedLeslie is now pointing to that object. It has a reference to that new object.

However, there is still a problem because using this technique of Object.assign only works on the first level. That is, if we have an object inside the leslie object for example, then this inner object will still be the same. It will still point to the same place in memory. That's why we say that the Object.assign method only creates a shallow copy and not a deep clone which is what we would like to have.

So, again, a shallow copy will only copy the properties in the first level, while a deep clone would copy everything. Let me illustrate this so that you can actually understand what I mean.

script.js
// An array is basically an object behind the scenes

const leslie = {
  firstName: 'Leslie',
  lastName: 'Alexander',
  age: 24,
  family: ['Dries', 'Tom']};


// Here were are now manipulation the array object

marriedLeslie.family.push('Courtney')
marriedLeslie.family.push('Lindsay')

console.log('Before marriage: ', leslie);
console.log('After marriage: ', marriedLeslie);

Primitives vs Objects - Practice - 5

From the above result, we see that both objects now have a family with four members. The last name of course was preserved because that's in the first level and Object.assign took care of copying that properly. And so that was not changed as we changed the last name in the copy (marriedLeslie).

However, the family object is deeply nested. Therefore, Object.assign did not really, behind the scenes, copy it to the new object. So, in essence, both the objects, leslie and marriedLeslie have a property family, which points at the same object in the memory heep, and that object is, of course, the array ['Dries', 'Tom']. Hence, changing the array in one of them will also be changed in the other one.

Now, a deep clone is what we would need here, but it is not easy to achieve, and it would actually be beyond the scope of this lecture to learn how to create a deep clone. Usually we achieve that by using an external library like for example, Lodash, which is a library with a ton of helpful tools, one of which is for deep cloning. And actually we will do that in a later section so that you see how we can include an external library to do this kind of stuff.

With this, we finish this section about how JavaScript works behind the scenes. It was a long one with so many thing and so many new concepts to learn. Many of them were hard and probably confusing, but that's not a problem. That's actually part of learning, and even if you did not understand 100% of everything, you're still good to move on in the course to the next section.