Mutations in javascript although simple, can be very tricky to understand.
In this write-up, you'll learn to, assign values to a variable, make shallow copies in javascript, and also you'll understand how mutations work and how to avoid them.
To understand mutations, let's understand the different types of data types available and how variables are assigned and then take it from there.
Without further ado let's dive in.
Javascript data types.
Data types are the different types of values a variable can store. In javascript, there are two distinct categories of data types, primitive data types, and user-defined or object types.
Primitive data types.
In javascript, primitive data types are implemented at the core of the language. Primitive values in javascript include:
- string. A sequence of characters. For example, "Hello world!", "Toby" etc.
- number. Smaller integer values including decimals. For example -0.3, 5, 10, etc.
- bigint. Larger integer values or numbers of great length
- Boolean. For example, true or false
- undefined.
- symbol.
- null.
In javascript, primitive values are immutable and do not have any methods although they behave as if they do.
let's understand it by looking at the code blocks below:
let numstr = 5..toString();
console.log(numstr) //Returns: 5
Notice that calling .toString()
method on 5 returns 5. Let's look at another example
var num = 5; // assigning a primitive of type number to variable num.
let numstr = num.toString(); // converts "num" to string.
console.log(numstr) //Returns 5
console.log(typeof numstr) //Returns string
In this example, you realize that calling .toString()
on the variable also returns 5, what happens is that javascript coerces the variable by wrapping the variable in a wrapper object and then calls the .toString
method on the wrapper object but not on the actual primitive value.
Reference / User-defined data types.
User-defined types are data types that are constructed by an author using primitive values and/or other user-defined types. Javascript user-defined types are all objects by default. These include:
- Objects literals
- Arrays
- Functions. Functions are a special type of object.
- Dates and any object defined using the new keyword.
Variables.
A variable is a named storage location. Variables point to a location containing a specific data type. For example, a fridge containing a snack, the fridge in this context is your "variable" and the snack is the value of a specific data type.
Assigning variables(primitive and reference types).
To assign a variable, you must declare it using any of these keywords const
, let
, and var
. For example,
const myVar = "something"
//or
let myVAr = "something"
//or
var myVay = "something"
In these examples "something" can be any of the data types you know. It's also common to see variables defined without being declared. For example,
myVar = "something"
This is allowed in javascript, because javascript is not static typed and also javascript hoists variables.
You can also assign a variable by referencing another variable. For example,
myVar = "something"
myVar1 = myVar // myVar1 is also equal to "something"
In javascript, assigning primitive values to variables like this does not mean myVar1 and myVar are pointing to the same storage location, instead myVar1 points to a different location containing the value it obtained from myVar. This means that changing the value in one variable does not affect the other.
However, assigning object types to variables behaves slightly differently.
Shallow Copy
Let's look at an example,
let obj1 = {name: "John", age: 10};
let obj2 = {name: "John", age: 10};
In this example, both obj1 and obj2 hold objects of similar values but are completely different, both objects point to different memory locations. Changing the values in obj1 does not change that of obj2. Let's look at another example that uses a reference to pass value,
let obj1 = {name: "John", age: 10};
let obj3 = obj1; // obj3 = {name: "John", age: 10};
let arr = [2, 4, 6, 8];
let arr2 = [...arr, 10] //arr2 = [2, 4, 6, 8, 10]
In this example obj3 and arr2 make a shallow copy of obj1 and arr respectively, which means they're pointing to the same memory location, any selective change to either name or age properties of either object variable affects the value of the other object variable, and also changing the values in arr affects arr2.
Note: Passing javascript objects by reference, or using the spread operator creates a shallow copy of the source variable.
In the same context as the latter example let's look at this example
obj3.name = "Doe"; // obj3 becomes {name: "Doe", age: 10};
console.log(obj1) // Returns {name: "Doe", age: 10}
You realize that changing the value of the name property in obj3 changes the value for obj1.name.
Mutations.
Mutation means a change in the original code or data.
Note: Primitive values cannot be mutated. They're read-only. For example,
let num = 5;
let str = "Hello";
num = 8;
console.log(num); //Returns 8 as the new value for num
str[0] = "p"
console.log(str); //Returns Hello
In the example, notice that it's possible to reassign a primitive value to a variable as in line 3
but it's not possible to change the value "h" at str[0]
to p. Let's look at another example involving strings.
let str = "hello";
str = "Hey"
console.log(str); //Returns Hey
In this example, we reassigned the str variable a value of "Hey" and did not just replace a character in the old string.
Mutating object types.
Note: Using const
to declare a variable means it cannot be reassigned to a new value for both primitive and object data types. However, for object types, their values can be mutated. For example,
const obj = {
name: "John",
Subscribers:50
}
const arr = [{name:"Tutor", Subscribers: 400}, {name:"Doe", Subscribers: 10}]
obj = arr[0] // Returns TypeError: Assignment to constant variable.
Notice the error message says it's not possible to assign a new value.
Now, let's look at an example of mutating a const variable,
const obj = {
name: "John",
Subscribers:50
}
const obj2 = obj;
obj2.name = "Dave"
console.log(obj) //Returns { name: 'Dave', Subscribers: 50 }
Notice that the value of the name property in obj has changed or better has been mutated. A metaphor for these examples will be, signing up for an account on Gmail with a name that's already been taken. It throws an error saying "Username already taken"(variable space already filled) but the owner of that specific account can change his username and it updates his details with the change.
Let's look at another example of mutation using the array from the previous example,
const arr = [{name:"Tutor", Subscribers: 400}, {name:"Doe", Subscribers: 10}]
const newArr = [...arr] //making shallow copy
newArr[0].name = "Clement"
console.log(newArr[0]) //Returns {name:"Clement", Subscribers: 400}
console.log(arr[0]) //Returns {name:"Clement", Subscribers: 400}
The example returns a new name for the first array object, you realize that although the name changed other values did stay the same and the object at that location remains the same object even if we selectively change the number of subscribers too.
Now that we've covered mutation let's look at the opposite or what's not considered a mutation.
Using the same example,
const arr = [{name:"Tutor", Subscribers: 400}, {name:"Doe", Subscribers: 10}]
const newArr = [...arr]
newArr[0] = {name: "Clement", Subscribers: 0}
console.log(newArr) //Returns [{name: "Clement", Subscribers: 0}, { name: 'Doe', Subscribers: 10 }]
console.log(arr) //Returns arr unchanged
Notice the difference in these examples on line 3
, In the latter example the position newArr[0] has been assigned a new object. A better metaphor to understand these two examples will be, Taking your Jeep to a garage for painting. Although the Jeep comes out with a different color it remains the same old Jeep(mutating an object). However, a new Lambo entering the same garage after the Jeeps leaves represents a completely different vehicle in the garage(reassign an object).
Note: To avoid mutating variables mistakenly, always reassign values to shallow copies of object types
Wrapping Up.
In this article, we've covered data types, variables, how to assign and reassign variable values, how to make shallow copies of objects and arrays, how to mutate objects, and how to avoid mutating objects.
Did you enjoy this article? If so, get more similar content by following me. Thanks for reading. Happy coding.
Top comments (8)
Thank you for your post. I was not aware of some details. I assume, some effects are more a result of a sloppy language design and not indended:
If primitive mutation was not intended, the last line should throw an error. It just does - nothing.
Same is for const objects. 'const' means "immutable", but as you mentioned, is is not. This works fine:
There is not difference to 'let h = {...' unless you do not try to assigne h. We can understand, why this is the case (because objects are adressed by reference, primitives by value), but it is far from what we should expect from a const value.
You wrote: "...also javascript hoists variables.". This would mean, you can access a variable before definition, which is not true for variables. What did you mean by this?
Although Javascript hoists all declarations, "const" and "let" are not initialized right away, hence you cannot access them before the declaration. However variables defined with "var" are pre-initialized with undefined.
I assume, you are right, but It does not seem to have any logic:
here we read:
This might have some logic for const, but not for let...
The spread operator creates a deep copy of first level (primitive) data and a shallow copy of the nested data.
You're getting it wrong Mehrzad, on your third line you made a change to your nested array at
y[1][0]
but kept the array copied from x, which in javascript means "hey I want a different value there but can you keep the rest as is". On your second line though, you're making an assignment specific to y at y[0]. Read MDN's article on deep copy for more clarification.You can call methods directly on numbers just fine... I based a whole library around this fact.
5.toString()
returns a syntax error because text isn't valid after a decimal point - which is part of the number.5..toString()
works just fine, as does5['toString']()
.Thanks for the correction! I really appreciate itπ
Additionally, you can read this material with pictures. It helps me to fully understand the concept.