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';
To understand what happened here, we need to go right-to-left:
- We create the string
"Julie"
- We create the variable
name
- 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';
Again, the play-by-play:
- We create the string
"Julie"
- We create the variable
name
- We assign the variable
name
a reference to the string we previously created - We create the string
"Jack"
- 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"
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" }
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"
Not very surprising. To be thorough, let's recap the last snippet in more detail:
- We create the string
"Mindy"
- We create the variable
name
and assign it a reference to the string"Mindy"
- We create the variable
name2
and assign a reference to the string"Mindy"
- We create the string
"Mork"
and reassignname2
to reference that string - When we
console.log
name
andname2
, we find thatname
is still referencing"Mindy"
andname2
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" }
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:
- We create the object
{ name: "Jack" }
- We create the
person
variable and assign it a reference to the created object - We create the
person2
variable and set it equal toperson
, which is referring to the previously-created object. (Note:person2
is now referencing the same object thatperson
is referencing!) - We create the string
"Jill"
and mutate the object by reassiging thename
property to reference"Jill"
- When we
console.log
person
andperson2
, 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" }
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"
}
}
*/
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));
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);
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:
Top comments (1)
Fantastic concept breakdown!