The concept of memory is very important in Javascript and today we will deep dive into this. At the end this concept of memory allocation will be helpful to understand another important concept of serialization and deserialization.
There are two types of memory allocation one is Stack and another is Heap memory. In JavaScript, primitive values (such as numbers, strings, booleans, etc.) are stored in the stack, while objects (such as arrays, objects etc.) are stored in the heap.
Primitive values are immutable and have a fixed size, so they can be easily stored and accessed in the stack. Objects are mutable and have a variable size, so they need to be stored and accessed in the heap, which is more flexible but also slower.
The main difference between stack and heap memory is stack memory is fixed and can't grow over but on the other hand heap memory can grow. For example, if you write an array or object the length of the array can be of any value. It can be 5 or 50 i.e. it can grow.
Let's see some example to get e more in depth understanding.
let fname = "John"; // string literal, stored in stack memory
let lname = "Doe"; // string literal, stored in stack memory
let newfname = fname
console.log(`First Name: ${fname}, Last Name: ${lname}`); // Accessing string literals from stack memory
console.log(`New First Name: ${newfname}`); // Accessing new variable from stack memory
//output
First Name: John, Last Name: Doe
New First Name: John
Here, we have created a variable fname
and assigned a value to it. Then we created another variable newfname
and made it equal to fname
.
Let's play with this.
let fname = "John"; // string literal, stored in stack memory
let lname = "Doe"; // string literal, stored in stack memory
let newfname = fname
console.log(`First Name: ${fname}, Last Name: ${lname}`); // Accessing string literals from stack memory
console.log(`New First Name: ${newfname}`); // Accessing new variable from stack memory
newfname = "Jane"; // Reassigning newfname, now it points to a new string literal in stack memory
console.log(`Updated First Name: ${fname}`); // Accessing original fname
console.log(`Updated New First Name: ${newfname}`); // Accessing updated newfname
//output
First Name: John, Last Name: Doe
New First Name: John
Updated First Name: John
Updated New First Name: Jane
Now we have made some changes to newfname
and print the variables. As expected the newfname
is different than fname
This is stack memory where a seperate memory has been allocated to variables and a change in one varible does not affect the other.
Let's go one step furthur and see another example
let person = {
fname: "John",
lname: "Doe"
};
let newPerson = person; // newPerson references the same object in heap memory
newPerson.fname = "Jane"; // Modifying the object through newPerson
newPerson.lname = "Smith"; // Modifying the object through newPerson
console.log(`Person First Name: ${person.fname}, Last Name: ${person.lname}`); // Accessing modified object
console.log(`New Person First Name: ${newPerson.fname}, Last Name: ${newPerson.lname}`); // Accessing modified object through newPerson
//output
Person First Name: Jane, Last Name: Smith
New Person First Name: Jane, Last Name: Smith
Like previous example, I we have created an object here person
and assigned the same to newperson
. But to the most surprise when we changed the newPerson
the changes has been done to person
variable too which is quite different than previous case!
The answer is heap memory. All the variables that can grow like arrays, objects are stored in heap memory and the address of the variable is stored in the stack memory. As a result person
is actually the address from the memory. Actually for this case when you do let newPerson = person
the address of the person
have been assigned to newPerson
and eventually both person
and newperson
points to same variable thorugh the same address in the heap memory. As a result when you make changes in one variable the other one changes.
I hope the difference of stack and heap memory is clear now. Now we will deep dive into another interesting topic.
Deep Copy and Shallow copy
In the previous example with person and newPerson, we saw that when you assign one object to another, any change in one reflects in the other. This is because both variables point to the same address in the heap memory, not separate ones.
That’s called a shallow copy — where only the reference (i.e., memory address) is copied, not the actual object.
Let’s make it more interesting with another example:
let person1 = {
fname: "John",
lname: "Doe",
details: {
age: 30,
city: "New York"
}
};
let person2 = person1; // shallow copy
person2.fname = "Jane";
person2.details.age = 25;
console.log(person1);
console.log(person2);
//output
{
fname: 'Jane',
lname: 'Doe',
details: { age: 25, city: 'New York' }
}
{
fname: 'Jane',
lname: 'Doe',
details: { age: 25, city: 'New York' }
}
As you can see, changing person2
also changes person1
— even the nested property details.age
. That’s because both person1
and person2
refer to the same object in heap memory.
A Better Way? Using Object Spread ...
let person2 = { ...person1 };
This is also a shallow copy. It only creates a new top-level object, but nested objects are still shared via reference.
So, modifying a nested property will still affect the original:
let person2 = { ...person1 };
person2.details.city = "Chicago";
console.log(person1.details.city); // Chicago
Again, same problem — nested values are not copied deeply.
Enter Deep Copy — the Reliable Way
To really copy the entire structure, including nested objects, we need a deep copy.
One simple way (especially for objects with no functions or special types) is to use JSON.stringify() and JSON.parse():
let person1String = JSON.stringify(person1);
let person2 = JSON.parse(person1String); // Deep copy
This method works because:
JSON.stringify() turns your object into a string (i.e., serializes it)
JSON.parse() creates a brand-new object from that string (i.e., deserializes it)
Now, person2 is completely separate from person1.
person2.details.city = "Los Angeles";
console.log(person1);
// Original object remains unchanged
console.log(person2);
// Shows updated city
//output
{
fname: 'John',
lname: 'Doe',
details: { age: 30, city: 'New York' }
}
{
fname: 'John',
lname: 'Doe',
details: { age: 30, city: 'Los Angeles' }
}
So, What’s Happening Here?
This is where serialization and deserialization comes in.
Serialization means converting your object into a string format (JSON.stringify()), which can be stored or sent over a network.
Deserialization means converting that string back into an object (JSON.parse()), often creating a new one in memory.
This concept is not only useful for making deep copies, but also essential when working with:
- APIs and network requests
- Local storage in the browser
- Sending or saving data
Wrapping It Up
Let’s quickly summarize what we’ve learned:
Stack memory is used for primitive types (string, number, boolean), and every variable gets its own copy.
Heap memory is used for objects (arrays, objects, functions), and variables store references (addresses).
Shallow copy copies the reference, not the actual object.
Deep copy duplicates the entire object structure — including nested objects — often using JSON.stringify() and JSON.parse().
Serialization and deserialization are powerful tools not just for copying, but for transmitting and storing complex data.
Understanding how memory works in JavaScript can save you from confusing bugs and give you a solid foundation for working with real-world apps.
Thanks for reading! 🚀
Top comments (0)