DEV Community

Cover image for Ultimate Guide to Mutations in Javascript.
Frank Otabil Amissah
Frank Otabil Amissah

Posted on

Ultimate Guide to Mutations in Javascript.

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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};
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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 }
Enter fullscreen mode Exit fullscreen mode

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}

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
efpage profile image
Eckehard • Edited

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:

let str = "Hello"
let a = str[0]  // -> "H"
str[0] = "x" // -> does nothing
Enter fullscreen mode Exit fullscreen mode

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:

    const h = {Name: "Alex"}
    h.age = 10 // {Name: "Alex", age: 10}
Enter fullscreen mode Exit fullscreen mode

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?

Collapse
 
amissah17 profile image
Frank Otabil Amissah

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.

Collapse
 
efpage profile image
Eckehard

I assume, you are right, but It does not seem to have any logic:

let a=7 // declared and initialized
let x // declared and not initialized
x = 7 // Declared and usable
y = 7 // Declared and known, but not usable?
let y // declared and not initialized
Enter fullscreen mode Exit fullscreen mode

here we read:

Meaning: The block of code is aware of the variable, but it cannot be used until it has been declared. What kind of "awareness" should this be?

This might have some logic for const, but not for let...

Collapse
 
merzaad profile image
Mehrzad Hosseini • Edited

The spread operator creates a deep copy of first level (primitive) data and a shallow copy of the nested data.

const x = [1, [1,2]]
const y = [...x]
y[1][0] = 2
y[0] = 2
console.log(x) // [1, [2,2]]
console.log(y) // [2, [2,2]]
Enter fullscreen mode Exit fullscreen mode
Collapse
 
amissah17 profile image
Frank Otabil Amissah

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.

Collapse
 
jonrandy profile image
Jon Randy πŸŽ–οΈ • Edited

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 does 5['toString']().

Collapse
 
amissah17 profile image
Frank Otabil Amissah

Thanks for the correction! I really appreciate itπŸ™

Collapse
 
sergo profile image
Sergo

Additionally, you can read this material with pictures. It helps me to fully understand the concept.