Memory: primitives vs objects
Build an honest mental model for JavaScript memory: stack frames, heap objects, references, reachability, and garbage collection basics.
- 01Use the model carefullyDescribe the stack and the heap as a way to think, not as a map of real memory.
- 02Predict shared objectsTell when a copy is separate and when two names share one object.
- 03Reason about how long objects liveUse reachability to spot memory leaks, and explain why a cycle can still be cleaned up.
Where values live, and why copying an object shares it
In the Data types lesson, you met two kinds of values:
- Primitives: simple values such as numbers, strings, booleans,
null, andundefined. - Objects: everything else, such as arrays, functions, and plain objects.
In the Objects & references lesson, you saw that copying an object variable doesn't copy the object. Both variables then share one object. So a change made through one name shows up through the other name too. This lesson gives you a picture of memory that explains why that happens.
The picture has two places:
- The stack: each function that is running gets a small area here, called a frame. The frame holds that function's variables.
- The heap: a big storage area where objects live.
This stack-and-heap picture is a mental model: a simple way to think that helps you predict what code does. It is not an exact map of real memory. The engine is the program that runs your JavaScript, such as V8 in Chrome. An engine may store values however it likes, as long as your code gives the same results. So use the model to predict results. Don't use it to guess where things really sit in memory.
In the model, each running function has a frame on the stack, and the frame holds its variables. A variable that holds a primitive holds the value itself. A variable that holds an object holds a reference: a way to find the object in the heap. When nothing can find an object any more, the garbage collector may clean it up. The garbage collector is the part of the engine that frees memory you no longer use.
let score = 3;
let nextScore = score;
nextScore = 4;
console.log(score, nextScore);Line 1 puts the number 3 in score. Line 2 copies that 3 into a new variable, nextScore. Line 3 puts 4 in nextScore only. So line 4 prints 3 4. The two variables never share anything.
nextScore. Line 3 puts 4 in that box only. The score box still holds 3, so line 4 prints 3 4.The stack and the heap, step by step
STEP THROUGHYou keep small notes for today's task on your desk. Bigger things go on numbered shelves in a storage room. A note that says "shelf 1" doesn't hold the parcel. It tells you where to find the parcel.
- In real life: Your desk, with notes for today’s task
- In JavaScript: The stack: one frame for each running function
- In real life: A note that says
a: 1 - In JavaScript: A variable that holds a primitive value
- In real life: A parcel on shelf 1 of the storage room
- In JavaScript: An object in the heap
- In real life: A note that says
user: shelf 1 - In JavaScript: A variable that holds a reference
Step through the program below. First, watch a number get copied. Then watch two variables point to the same object. Line 3 uses let user, not const, because line 6 gives user a new value, null. A const variable can't get a new value.
A replay of the stack-and-heap model, recorded from real JavaScript values. Guess what line 7 prints, then step through each variable and reference.
script
let b = a;let user = { name: "Ada" };const other = user;other.name = "Lin";user = null;console.log(other.name);Look closely at line 5. other.name = "Lin" doesn't change what other holds. JavaScript follows the reference in other to the object, and changes the object's name. The variable user holds the same reference, so user.name would also give "Lin".
user and other each hold the same reference, #1. Both point to one object in the heap. Line 5 changed that one object, so reading the name through either variable gives "Lin".Line 6 sets user to null. That changes only the variable user. The object is still in the heap, and other still points to it. So line 7 prints Lin.
Copying a value vs copying a reference
COMPAREPeople often say, "Primitives are copied by value, and objects are copied by reference." That is close. Here is a clearer rule:
Every variable holds a value, and for an object, that value is a reference.
When you copy a variable, you copy whatever it holds:
- For a number, you get a second number.
- For an object, you get a second reference to the same object.
The model is simpler than what engines really do. The table compares the two. Read each row as: "the model shows it like this, but an engine may really do this."
| What kind of value | How the model shows it | What an engine may really do |
|---|---|---|
Primitives, like 1, true, or null | The variable holds the value itself. | The engine may keep a small number right inside the variable, keep it in a fast spot inside the processor, or store a number as a small object on the heap. Your code can't tell which. |
| Objects and arrays | The variable holds a reference to an object in the heap. | The engine may move the object to a new spot to save space. A reference is not a memory address you can print. |
| Closures: functions that remember outer variables | When a function finishes, its frame is gone. Variables that an inner function still uses stay available. | The engine keeps those variables somewhere that lasts after the call, often on the heap. |
| Strings | Strings act like primitive values. | The engine may let equal strings share one stored copy. Your code still sees strings as values that never change. |
Giving a variable a new value with = changes only that variable. This is called reassignment. Changing a property, like other.name = "Lin", changes the object itself. This is called mutation. If two variables refer to the same object, both see a mutation.
const cart = { item: { name: "tea", quantity: 1 } };
const copy = { ...cart };
copy.item.quantity = 2;
console.log(cart.item.quantity);Line 1 makes a cart object. Its item property holds another object, called a nested object. Line 2 uses spread, ...cart, to make a new outer object called copy. Spread copies the value of each property. The value of item is a reference. So copy.item points to the same nested object as cart.item. Line 3 changes that shared nested object, so line 4 prints 2.
A copy like this is called a shallow copy: only the outer object is new. It works well when you change one top-level property. When the nested objects must be separate too, you need a deep copy, which copies every level.
Parameters get a copy too
STEP THROUGHA parameter is the variable a function gets for each value you pass in. Function calls follow the same rule as =. The parameter gets a copy of the value you pass. If that value is a reference, the copy points to the same object. Some people call this call by sharing.
function addTax(price) {
price = price + 1;
return price;
}
let price = 3;
console.log(addTax(price), price);The function's parameter is called price, and so is the variable outside it. They are two separate variables. Line 5 stores 3 in the outside price. Line 6 calls addTax(price), so the parameter gets its own copy of 3. Line 2 changes that copy to 4, and line 3 returns it. The outside price is still 3. So line 6 prints 4 3.
A parameter gets a copy of the value you pass. For an object, that copy is a reference: changing the object shows outside, but pointing the parameter somewhere new does not.
script
function rename(person) { person.name = "Lin"; person = { name: "Grace" }; return person.name;} const result = rename(user);console.log(user.name, result);Line 2 changes object #1 through person. The caller's user points to the same object, so user.name becomes "Lin". Line 3 makes a new object, #2, and points only person at it. The caller's user still points to #1. So line 9 prints Lin Grace.
rename. Line 2 changed object #1, which user still points to. Line 3 made object #2 and pointed only person at it. So line 9 prints Lin Grace.Line 2 is the pattern behind many "Why did this function change my object?" bugs.
Reachability: can JavaScript still find the object?
INTERACTIVEWhen may the engine throw an object away? It checks whether the object is reachable:
- It starts from roots. Roots are places the engine can always see, such as global variables and the variables of functions that are running right now.
- It follows every reference from the roots, then every reference from those objects, and so on.
- Any object it gets to is reachable, so it must stay.
- Any object it can't get to is collectible: the garbage collector is allowed to clean it up.
Start with the people in the group. Follow the contacts they shared, then the contacts those people shared. Anyone you can reach this way stays. Anyone you can't reach can be deleted.
- In real life: People already in the group
- In JavaScript: Roots: global variables and the variables of running functions
- In real life: A contact someone shared in a message
- In JavaScript: A reference from one object to another
- In real life: Someone nobody in the group can reach
- In JavaScript: An object no root can reach
- In real life: Deleting contacts nobody can reach
- In JavaScript: The garbage collector cleaning up
function makeProfile(name) {
return { name };
}
const profile = makeProfile("Asha");
console.log(profile.name);Line 4 calls makeProfile("Asha"). The function makes an object and returns a reference to it. Line 4 stores that reference in profile. The function has finished, and its frame is gone. But the object is still reachable through profile, so it stays. Line 5 prints Asha.
let globalUser = { profile: { city: "Kochi" } };let modal = { owner: globalUser };let a = { name: "A" };a.friend = { name: "B", friend: a };a = null;modal = null;globalUser = null;Roots
- globalUser → user
- modal → modal
Objects in the heap
- user object
reachable
points to: profile - profile object
reachable
points to nothing - modal object
reachable
points to: user - object A
unreachable: can be cleaned up
points to: b - object B
unreachable: can be cleaned up
points to: a
Roots are starting points, such as global variables. Any object you can get to by following references from a root is reachable. Line 5 set a to null, so no root reaches A or B any more.
Look at objects A and B. Line 4 makes them point to each other. Objects that point to each other in a loop form a cycle. Line 5 sets a to null, so after that no root points to A or B.
Modern garbage collectors use mark-and-sweep. First they mark every object they can reach from the roots. Then they sweep away, or clean up, every object that isn't marked. They never reach A or B, so both can be cleaned up together.
globalUser still reaches the user object and its profile, so they stay. A and B point to each other, but no root reaches them, so both can be cleaned up. The modal object, cut on line 6, can be cleaned up too.An older method, reference counting, counts how many references point to each object. It frees an object when its count reaches zero. In a cycle, A and B each keep the other's count at 1. So reference counting alone would never free them. The later Reference counting & cycles lesson covers this.
window.currentUserpoints to an object- A function makes an object and returns it to the caller, who stores it
- Two objects point to each other, but no root points to either one
- An array slot was set to
null, and no other reference to the old object exists - A
Mapat the top level of a file still stores an object as a value - Only a
WeakMapkey refers to an object
Sort each case by whether a root can still reach the object. Collectible means it may be cleaned up, not that it is cleaned up right away.
Garbage collection basics
Garbage collection means the engine frees memory for you. Your program makes objects as it runs. Every so often, the engine finds objects that are not reachable and reuses their memory. Your code can't see the exact moment an object is freed. So don't write code that depends on that moment.
Many engines also use generational collection. Most objects are needed only for a short time. So the engine checks new objects more often than old ones. Engines may also move the objects that are still in use closer together, to fill the gaps. The engine makes these choices. Your code can't ask for them for one object.
The table explains the main words. The last column shows how to talk about each one without saying something wrong.
| Word | What it means | How to say it carefully |
|---|---|---|
| Roots | Starting points the engine can always see: global variables, the variables of running functions, and references the engine keeps itself. | If a root can reach an object, the object must stay. |
| Mark-and-sweep | First mark every object you can reach from the roots. Then sweep away every object that isn't marked. | This is why a cycle that no root reaches can still be cleaned up. |
| Generational collection | Most objects are needed only briefly, so the engine checks new objects more often than old ones. | The engine does this to save time. You can't control it from your code. |
| Collectible | No root can reach the object, so it may be cleaned up. | Don't say it is cleaned up the moment a variable becomes null. |
Your browser's DevTools has a Memory panel. It can take a heap snapshot: a list of every object in memory at that moment. Take two snapshots a while apart and compare them. Objects that keep piling up point to a memory problem. The pictures in this lesson are models. A heap snapshot shows what is really there. The later Finding memory leaks lesson goes deeper.
Where you’ll use this
You'll use this model in everyday frontend work:
- Fixing an object that changed when you didn't expect it.
- Choosing between
MapandWeakMap. - Cleaning up timers and event listeners.
- Finding out why a web app gets slower after you move between many of its pages.
const seenUsers = new Map();
function rememberUser(user) {
seenUsers.set(user.id, user);
}
function clearFinishedUsers() {
seenUsers.clear();
}A cache is a store of data you keep so you don't have to fetch or build it again. Line 1 makes the cache: a Map at the top level of a file. That Map stays reachable as long as the file is loaded. A Map keeps every value it stores reachable too. So a cache that only grows is a memory leak: memory that stays in use even though you no longer need the data. Line 8 fixes it: seenUsers.clear() removes every entry.
const recentUsers = new Map();
function remember(user) {
recentUsers.set(user.id, user);
if (recentUsers.size > 2) recentUsers.delete(recentUsers.keys().next().value);
}
remember({ id: "a" });
remember({ id: "b" });
remember({ id: "c" });
console.log(recentUsers.size);This version keeps only the two newest users. Line 3 adds a user. On line 4, if there are more than 2 entries, it deletes the oldest key. A Map remembers the order its keys were added, so recentUsers.keys().next().value is the oldest key. The third call, on line 8, adds "c" and deletes "a". So line 9 prints 2. A size limit works well when data is useful for a while but shouldn't grow forever.
const metadata = new WeakMap();
function rememberElement(element, info) {
metadata.set(element, info);
}If the extra data belongs to an object, a WeakMap may be better. A WeakMap uses objects as keys, and it holds them weakly: the key alone doesn't keep the object reachable. When nothing else refers to element, the garbage collector may clean up both the element and its info.
More habits that prevent leaks:
- A timer or event listener you forget about keeps its function reachable, and everything that function uses. Clear it when you're done.
- Arrays,
Maps, and caches that keep growing need a size limit, deletion, or clearing. - A page element you removed from the page stays in memory if a JavaScript variable still points to it. Drop that reference too.
- A closure, a function that remembers outer variables, can keep large data alive. Keep what it remembers small, or let go of it when the work is done.
- A
constobject variable can't get a new value with=, but you can still change the object's properties. - Clean up the same thing you set up: remove a listener with the same function you added, and clear a timer you started.
Common misconceptions
“Primitives always live on the stack.”
That is a simplification. JavaScript promises that primitives are copied as values. Where they are really stored is up to the engine.
“An object variable contains the whole object.”
It holds a reference to the object. Copying the variable copies the reference, not the object.
“const makes an object unchangeable.”
const only stops the variable from getting a new value with =. You can still change the object's properties, and the objects inside it.
“Setting a variable to null frees the object.”
It removes one path to the object. If a root can still reach the object another way, it stays. Even when nothing reaches it, you can't tell when it is cleaned up.
“Cycles always leak.”
Mark-and-sweep can clean up a cycle that no root reaches. Leaks happen when something reachable keeps growing.
“Strings must be stored on the stack.”
In JavaScript, strings are primitive values that never change. Where the engine stores them, and whether equal strings share storage, is up to the engine.
“A WeakMap makes values disappear right away.”
A WeakMap only stops its keys from keeping objects alive. It doesn't change when cleanup happens, and you still can't predict that moment.
Practice: predict and fix memory behavior
5 EXERCISESTrace the code and type the exact value it prints.
let user = { name: "Ada" };
let other = user;
other.name = "Lin";
user = null;
console.log(other.name);It prints Lin. user and other pointed to the same object. user = null removes one path, but other still points to the object.
In the reachability playground, look at the last step, after both modal and globalUser are null. Is the nested profile object reachable or collectible?
Collectible. No root can reach profile, so the garbage collector may clean it up. You can't know when that will happen.
Two objects point only to each other. Every outside reference to them is gone. Can they be cleaned up?
Yes. Mark-and-sweep never reaches A or B from the roots, so neither one is marked, and both can be cleaned up. Reference counting alone would never free them, because each one keeps the other's count at 1.
A Map at the top level of a file grows every time a user opens a profile. Name one fix.
const seenUsers = new Map();
function rememberUser(user) {
seenUsers.set(user.id, user);
}
function clearFinishedUsers() {
seenUsers.clear();
}seenUsers.clear();
// or, for object-keyed metadata:
const metadata = new WeakMap();Clear the Map, or delete the entries you no longer need. If the cache stores extra data keyed by objects, a WeakMap won't keep those key objects alive by itself.
A function should add a role to a person without changing the object it receives. What kind of value should it return?
function label(person) {
return { ...person, role: "admin" };
}function label(person) {
return { ...person, role: "admin" };
}Returning { ...person, role: "admin" } makes a new object. The caller's object doesn't change.
Quiz: check your memory model
7 QUESTIONSQuestion 1 of 7In this lesson, what is the stack-and-heap picture?
Choose an answer to see the explanation.
Question 2 of 7What does this print?
Read the code, then predictlet a = 1; let b = a; b = 2; console.log(a);Choose an answer to see the explanation.
Question 3 of 7Two variables point to one object. What does this print?
Read the code, then predictconst user = { name: "Ada" }; const other = user; other.name = "Lin"; console.log(user.name);Choose an answer to see the explanation.
Question 4 of 7A function gets an object as its parameter
person. Which line changes the caller's object?Choose an answer to see the explanation.
Question 5 of 7Two objects point only to each other, and no root reaches either one. What happens to them?
Choose an answer to see the explanation.
Question 6 of 7You want to store extra data about an object, but the data shouldn't keep the object alive. Which store fits?
Choose an answer to see the explanation.
Question 7 of 7Which sentence about garbage collection is true?
Choose an answer to see the explanation.
Key takeaways
- The stack-and-heap picture is a model for thinking, not an exact map of real memory.
- Copying a primitive makes a separate value. Copying an object variable copies a reference to the same object.
- Changing a property changes the object. Using
=on a variable changes only that variable. - An object stays while a root can reach it. A cycle that no root reaches can still be cleaned up.
- The engine cleans up memory on its own schedule. Your code shouldn't depend on when.
Remember the one-liner.
An object stays alive while a root can still reach it. Copying an object variable copies the way to find the object, not the object.
Up next: Scope & closures.