Foundational JavaScript Concepts: Variable Assignment and Primitive/Object Mutability

nas5w

Nick Scialli (he/him)

Posted on March 22, 2020

Foundational JavaScript Concepts: Variable Assignment and Primitive/Object Mutability

If you're not familiar with how JavaScript variable assignment and primitive/object mutability works, you might find yourself encountering bugs that you can't quite explain. I think this is one of the more important foundational JavaScript topics to understand, and I'm excited to share it with you today!

JavaScript Data Types

JavaScript has seven primitive data types[1]:

  • Boolean (true, false)
  • Null (null)
  • Undefined (undefined)
  • Number (e.g., 42)
  • BigInt (e.g., 10000000000000000n)
  • String (e.g., "Hello world")
  • Symbol (e.g., Symbol(11))

Additionally, JavaScript has object data types. JavaScript has several built-in object data types, the most well-known and widely-used being Array, Object, and Function.

Assignment, Reassignment, and Mutation

Assignment, reassignment, and mutation are important concepts to know and differentiate in JavaScript. Let's define each and explore some examples.

Assignment

To understand assignment, let's analyze a simple example.

let name = 'Julie';
Enter fullscreen mode Exit fullscreen mode

To understand what happened here, we need to go right-to-left:

  1. We create the string "Julie"
  2. We create the variable name
  3. We assign the variable name a reference to the string we previously created

So, assignment can be thought of as the process of creating a variable name and having that variable refer to data (be it a primitive or object data type).

Reassignment

Let's extend the last example. First, we will assign the variable name a reference to the string "Julie" and then we will reassign that variable a reference to the string "Jack":

let name = 'Julie';
name = 'Jack';
Enter fullscreen mode Exit fullscreen mode

Again, the play-by-play:

  1. We create the string "Julie"
  2. We create the variable name
  3. We assign the variable name a reference to the string we previously created
  4. We create the string "Jack"
  5. We reassign the variable name a reference to the string "Jack"

If this all seems basic, that's okay! We're laying the foundation for understanding some more complicated behavior and I think you'll be glad we did this review.

Mutation

Mutation is the act of changing data. It's important to note that, in our examples thus far, we haven't changed any of our data.

Primitive Mutation (spoiler: you can't)

In fact, we wouldn't have been able to change any of our data in the previous example even if we wanted to—primitives can't be mutated (they are immutable). Let's try to mutate a string and bask in the failure:

let name = 'Jack';
name[2] = 'e';
console.log(name);
// "Jack"
Enter fullscreen mode Exit fullscreen mode

Obviously, our attempt at mutation failed. This is expected: we simply can't mutate primitive data types.

Object Mutation

We absolutely can mutate objects! Let's look at an example.

let person = {
  name: 'Beck',
};
person.name = 'Bailey';
console.log(person);
// { name: "Bailey" }
Enter fullscreen mode Exit fullscreen mode

So yeah, that worked. It's important to keep in mind that we never reassigned the person variable, but we did mutate the object at which it was pointing.

Why This All Matters

Get ready for the payoff. I'm going to give you two examples mixing concepts of assignment and mutation.

Example 1: Primitives

let name = 'Mindy';
let name2 = name;
name2 = 'Mork';
console.log(name, name2);
// "Mindy" "Mork"
Enter fullscreen mode Exit fullscreen mode

Not very surprising. To be thorough, let's recap the last snippet in more detail:

  1. We create the string "Mindy"
  2. We create the variable name and assign it a reference to the string "Mindy"
  3. We create the variable name2 and assign a reference to the string "Mindy"
  4. We create the string "Mork" and reassign name2 to reference that string
  5. When we console.log name and name2, we find that name is still referencing "Mindy" and name2 is referencing the string "Mork"

Example 2: Objects

let person = { name: 'Jack' };
let person2 = person;
person2.name = 'Jill';
console.log(person, person2);
// { name: "Jill" }
// { name: "Jill" }
Enter fullscreen mode Exit fullscreen mode

If this surprises you, try it out in the console or your favorite JS runtime environment!

Why does this happen? Let's do the play-by-play:

  1. We create the object { name: "Jack" }
  2. We create the person variable and assign it a reference to the created object
  3. We create the person2 variable and set it equal to person, which is referring to the previously-created object. (Note: person2 is now referencing the same object that person is referencing!)
  4. We create the string "Jill" and mutate the object by reassiging the name property to reference "Jill"
  5. When we console.log person and person2, we note that the one object in memory that both variables were referencing has been mutated.

Pretty cool, right? And by cool, I mean potentially scary if you didn't know about this behavior.

The Real Differentiator: Mutability

As we discussed earlier, primitive data types are immutable. That means we really don't have to worry about whether two variables point to the same primitive in memory: that primitive won't change. At best, we can reassign one of our variables to point at some other data, but that won't affect the other variable.

Objects, on the other hand, are mutable. Therefore, we have to be keep in mind that multiple variables may be pointing to the same object in memory. "Mutating" one of those variables is a misnomer, you're mutating the object it's referencing, which will be reflected in any other variable referencing that same object.

Is This a Bad Thing?

This question is far too nuanced to give a simple yes or no answer. Since I have spent a good amount of time understanding JavaScript object references and mutability, I feel like I actually use it to my advantage quite a bit and, for me, it's a good thing. But for newcomers and those who haven't had the time to really understand this behavior, it can cause some pretty insidious bugs.

How Do I Prevent This from Happening?

In many situations, you don't want two variables referencing the same object. The best way to prevent this is by creating a copy of the object when you do the assignment.

There are a couple ways to create a copy of an object: using the Object.assign method and spread operator, respectively.

let person = { name: 'Jack' };
// Object.assign
let person2 = Object.assign({}, person);
// Spread operator
let person3 = { ...person };
person2.name = 'Pete';
person3.name = 'Betty';
console.log(person, person2, person3);
// { name: "Jack" }
// { name: "Pete" }
// { name: "Betty" }
Enter fullscreen mode Exit fullscreen mode

Success! But a word of caution: this isn't a silver bullet because we're only creating shallow copies of the person object.

Shallow Copies?

If our object has objects nested within it, shallow copy mechanisms like Object.assign and the spread operator will only create copies of the root level object, but deeper objects will still be shared. Here's an example:

let person = {
  name: 'Jack',
  animal: {
    type: 'Dog',
    name: 'Daffodil',
  },
};
person2 = { ...person };
person2.name = 'Betty';
person2.animal.type = 'Cat';
person2.animal.name = 'Whiskers';
console.log(person);
/*
{
  name: "Jack",
  animal: {
    type: "Cat",
    name: "Whiskers"
  }
}
*/
Enter fullscreen mode Exit fullscreen mode

Ack! So we copies the top level properties but we're still sharing references to deeper objects in the object tree. If those deeper objects are mutated, it's reflected when we access either the person or person2 variable.

Deep Copying

Deep copying to the rescue! There are a number of ways to deep copy a JavaScript object[2]. I'll cover two here: using JSON.stringify/JSON.parse and using a deep clone library.

JSON.stringify/JSON.parse

If your object is simple enough, you can use JSON.stringify to convert it to a string and then JSON.parse to convert it back into a JavaScript object.

let person = {
  name: 'Jack',
  animal: {
    type: 'Dog',
    name: 'Daffodil',
  },
};
person2 = JSON.parse(JSON.stringify(person));
Enter fullscreen mode Exit fullscreen mode

And this will work... but only in limited situations. If your object has any data that cannot be represented in a JSON string (e.g., functions), that data will be lost! A risky gambit if you're not super confident in the simplicity of your object.

Deep Clone Library

There are a lot of good deep clone libraries out there. One such example is lodash with its _.cloneDeep method. These libraries will generally traverse your object and do shallow copies all the way down until everything has been copied. From your perspective, all you have to do is import lodash and use cloneDeep:

let person = {
  name: 'Jack',
  animal: {
    type: 'Dog',
    name: 'Daffodil',
  },
};
person2 = _.cloneDeep(person);
Enter fullscreen mode Exit fullscreen mode

Conclusion

This discussion is really the tip of the iceburg when it comes to variable assignment and data mutability in JavaScript. I invite you to continue researching this topic, experimenting with topics like equality comparison when assigning object references and copying objects.


References:

  1. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures
  2. https://stackoverflow.com/questions/122102/what-is-the-most-efficient-way-to-deep-clone-an-object-in-javascript
💖 💪 🙅 🚩
nas5w
Nick Scialli (he/him)

Posted on March 22, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related