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 stack frames and heap objects without pretending to inspect engine memory.
- 02Predict aliasingTell when a copy is independent and when two names share one object.
- 03Reason about lifetimeUse reachability to spot leaks and understand why cycles can still be collected.
Where values live, and why references matter
You already met Data types: primitives such as numbers, strings, booleans, null, and undefined, plus objects such as arrays, functions, and object literals. You also met Objects & references: copying an object variable copies a reference, so aliases can see the same mutation. This lesson goes one level deeper: it gives you a memory model for predicting why that happens.
The curriculum summary says it plainly: where values live, and why copying an object copies a reference. We will use the common stack-and-heap model because it is useful, but we will label it honestly. JavaScript engines may store small integers inline, box numbers on the heap, keep closure variables on the heap, compress pointers, or remove allocations entirely. You are learning a reasoning model, not peeking into V8 or SpiderMonkey memory.
In the model, a stack frame holds the current bindings, primitive values are copied as values, object variables hold references to heap objects, and the garbage collector can reclaim objects that are no longer reachable from roots.
Stack & heap mental model
STEP THROUGHKeep the notes for today’s task on your desk. Keep bigger things you need later in a storage room. A variable that “holds an object” holds the way to find that object.
- In real life: A desk for today’s work
- In JavaScript: The call stack: one frame per active function
- In real life: A note saying
a: 1 - In JavaScript: A binding holding a primitive value
- In real life: A storage room holding a box
- In JavaScript: A heap object
- In real life: A note with a box number
- In JavaScript: A variable holding a reference
Where the analogy stops: Engines can store and move values differently. Use this as a reasoning model, not a measurement.
Step through the program. Watch primitive copies stay independent, then watch two bindings point at the same object. Line 6 deliberately uses let user so we can set it to null; a const binding could not be reassigned.
A conceptual stack/heap replay recorded from real JavaScript values. Predict what line 7 prints, then step through each binding and reference.
script
let b = a;let user = { name: "Ada" };const other = user;other.name = "Lin";user = null;console.log(other.name);The important surprise is line 5. other.name = "Lin" does not change the reference stored in other. It follows that reference and changes the object. Since user had the same reference, user.name would see the change too until line 6 removes that one path.
Value vs reference
COMPAREJavaScript is often described as “primitives are passed by value, objects are passed by reference.” That phrase is close, but a sharper version is: every variable stores a value; for objects, that value is a reference. Copying the variable copies whatever value is in the binding.
| Thing | Mental model | Engine reality to remember |
|---|---|---|
| Primitives | The binding directly holds values like 1, true, or null. | Engines may store small integers inline, keep values in registers, or box some numbers. You cannot observe the exact storage from normal code. |
| Objects and arrays | The binding holds a reference to a heap object. | The object may move during compaction; the reference is an abstract language value, not a raw address you can print. |
| Closures | A finished stack frame is gone, but captured variables stay available. | Engines keep captured state somewhere it can outlive the call, commonly heap-allocated or optimized another way. |
| Strings | Strings behave like primitive values. | Engines may share or intern string storage. JavaScript still treats strings as immutable primitive values. |
Reassignment changes one binding. Mutation changes an object. If two bindings refer to the same object, both observe the mutation.
Function parameters copy references
STEP THROUGHFunction calls use the same rule. A parameter receives a copy of the argument value. If the argument value is a reference, the copy points at the same object. This is sometimes called call by sharing.
Parameters receive copies of values. With objects, that copied value is a reference, so mutation is visible but reassignment is local.
script
function rename(person) { person.name = "Lin"; person = { name: "Grace" }; return person.name;} const result = rename(user);console.log(user.name, result);Line 2 mutates object #1, so the caller sees user.name become "Lin". Line 3 only reassigns the local parameter to a new object #2, so it does not replace the caller’s user. This is the pattern behind many “why did this function change my object?” bugs.
Reachability: can anyone still find the box?
INTERACTIVEStart with people already in the group and follow shared contacts. If you can reach someone through that chain, JavaScript must keep the object. If not, it is eligible for cleanup.
- In real life: People already in the group
- In JavaScript: Roots: globals, current stack bindings, and engine-known references
- In real life: A contact shared in a message
- In JavaScript: Object-to-object references
- In real life: Nobody reachable through the group
- In JavaScript: An unreachable object
- In real life: Removing unused information
- In JavaScript: The garbage collector
Where the analogy stops: Messages do not control when a phone cleans storage. The model only says an object is eligible for collection.
let globalUser = { profile: { city: "Kochi" } };let modal = { owner: globalUser };let a = { name: "A" };let b = { name: "B", friend: a };a.friend = b;modal = null;globalUser = null;Roots
- globalUser → user
- modal → modal
Heap objects
- user object
reachable
references: profile - profile object
reachable
no outgoing references - modal object
reachable
references: user - cycle A
unreachable → eligible
references: b - cycle B
unreachable → eligible
references: a
Roots are starting points such as globals and current stack bindings. Anything found by following references from them is reachable.
Notice the cycle. Objects A and B point at each other, but no root points at A or B. Modern mark-and-sweep collectors handle that: they mark from roots, never reach A or B, and can collect the whole island. A pure reference-counting collector would struggle because each object keeps the other’s count above zero; that is the classic “Reference counting & cycles” problem.
window.currentUserpoints to an object- A function creates an object and returns it to the caller
- Two objects point at each other, but no root points at either one
- An array slot was set to
null, and no other reference exists - A module-level
Mapstill stores an object as a value - Only a
WeakMapkey refers to an object
Sort each scenario by whether a root can still reach the object. Eligible does not mean collected immediately.
Garbage collection basics
Garbage collection is automatic memory management. JavaScript creates objects as your program runs. The engine periodically finds objects that are not reachable and makes their memory available for reuse. Normal code cannot observe the exact moment an ordinary object is freed, and it should not depend on that timing.
| Idea | Meaning | Careful wording |
|---|---|---|
| Roots | Starting points such as global bindings, the current call stack, and engine-managed references. | If a root can reach an object, it must stay. |
| Mark-and-sweep | Mark everything reachable from roots, then sweep away unmarked objects. | This explains why unreachable cycles can be collected. |
| Generational GC | Most objects die young, so engines often collect young objects more often than old ones. | This is an optimization strategy, not a rule you control from code. |
| Eligibility | An unreachable object may be collected. | Do not say it is collected exactly when a variable becomes null. |
Browser DevTools Memory panels can take heap snapshots and help you find leaks. Use them to compare retained objects over time. Treat this lesson’s diagrams as conceptual models; the DevTools snapshot is the tool for investigation. The later Finding memory leaks lesson goes deeper.
Where you’ll use this
This model shows up in everyday frontend work: fixing accidental mutation, choosing Map or WeakMap, clearing subscriptions, and understanding why a page gets slower after many route changes.
const seenUsers = new Map();
function rememberUser(user) {
seenUsers.set(user.id, user);
}
function clearFinishedUsers() {
seenUsers.clear();
}A module-level Map is reachable as long as the module is reachable. Its entries strongly hold their values, so a cache that only grows is a leak pattern. If the cache is for temporary metadata attached to objects, a WeakMap may be better because weakly held keys do not keep objects alive by themselves.
const metadata = new WeakMap();
function rememberElement(element, info) {
metadata.set(element, info);
}- Forgotten timers or listeners can keep callbacks and captured data reachable. Clear them.
- Growing arrays, Maps, and caches need size limits, deletion, or clearing.
- Detached page nodes can be kept alive by ordinary JavaScript references. Drop those references.
- Closures can capture large data. Keep captured state small or release it when the work is done.
Common misconceptions
“Primitives always live on the stack.”
That is a simplification. The language behavior is value copying; engine storage is an implementation detail.
“An object variable contains the whole object.”
The useful model is that it contains a reference. Copying the variable copies that reference.
“Setting one variable to null frees the object.”
It removes one path. The object stays if another root can still reach it, and collection timing is not observable.
“Cycles always leak.”
Unreachable cycles are collectible with mark-and-sweep. Leaks happen when something reachable keeps growing.
“WeakMap makes values disappear immediately.”
WeakMap weakens keys, not timing. You still cannot rely on exact collection moments.
Practice: predict and fix memory behavior
5 EXERCISESTrace the code and type the exact value printed.
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. Setting user = null removes one path, but other still reaches the object.
In the reachability playground, after both modal and globalUser are null, what is the nested profile object: reachable or collectible?
profile is unreachable, so it is eligible for collection. We do not know when collection will actually run.
Can two objects that only reference each other be collected after all outside references are gone?
Yes. An unreachable cycle can be collected by mark-and-sweep because neither object is marked from the roots. Reference counting alone would leak this shape.
A module-level Map 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 or delete entries from a normal Map when the data is no longer needed. If the cache is metadata keyed by objects, WeakMap can avoid keeping those key objects alive by itself.
Rewrite the idea so a function adds a role 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" } creates a new object. The caller's original object is not mutated.
Quiz: check your memory model
7 QUESTIONSQuestion 1 of 7Which statement best describes the stack and heap in this lesson?
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 7What does the object alias 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 parameter receives an object. Which operation changes the caller's object?
Choose an answer to see the explanation.
Question 5 of 7What happens to two objects that only point at each other after all roots are cut?
Choose an answer to see the explanation.
Question 6 of 7Which cache is safer when data should not keep an object alive by itself?
Choose an answer to see the explanation.
Question 7 of 7Which claim about garbage collection is honest?
Choose an answer to see the explanation.
Key takeaways
- The stack/heap story is a mental model, not a literal map of engine storage.
- Primitive copies are independent values; object copies share references.
- Mutation changes an object; reassignment changes one binding.
- Reachability decides whether objects are still needed. Unreachable cycles can be collectible.
- Garbage collection timing is automatic and not something normal code should observe.
Remember the one-liner.
A JavaScript object stays alive while a root can reach it; copying an object variable copies the path, not the object.
Up next: Scope & closures.