const user = { name: "Alice", role: "user" };
const admin = user;
admin.role = "admin";
console.log(user.role); // What do you think this logs?
If you guessed "user", welcome to the club. Most developers get this wrong at first.
The answer is "admin". And if that surprises you, you're about to learn something that will change how you write JavaScript forever.
This isn't some edge case or weird quirk. This is fundamental to how JavaScript works. Once you understand what's happening behind the scenes in something called "the stack" and "the heap" suddenly everything clicks.
Those bugs that seemed random? They make sense. Those React state issues? Crystal clear. Those confusing code reviews? Actually straightforward.
Let me show you the mental model that most tutorials skip.
The Code That Doesn't Make Sense
Here's something you might have written (or are about to write):
function createUser(template) {
const newUser = template;
newUser.id = generateId();
return newUser;
}
const userTemplate = { name: "", role: "user", permissions: [] };
const alice = createUser(userTemplate);
alice.name = "Alice";
const bob = createUser(userTemplate);
bob.name = "Bob";
console.log(alice.name); // "Bob" ??? π±
Wait, what? Alice's name changed when we created Bob?
This looks like a bug. The code seems perfectly logical. But it's working exactly as designed, we just don't have the right mental model yet.
The problem isn't the code. It's that objects live in a completely different place than we think.
The Mental Model You Actually Need
Here's what's really happening when JavaScript runs your code:
Think of JavaScript Memory Like a Restaurant
The Stack = The Order Board
- Fast, temporary, organized
- New orders go on top, completed orders get removed
- Automatically cleared when done
- Holds: function calls, variables, and pointers
The Heap = The Storage Room
- Slower, permanent (until garbage collected), flexible
- Stores the actual data
- Accessed by reference (like a locker number)
- Holds: objects, arrays, functions, most strings
When you write const user = { name: "Alice" }, here's what JavaScript does:
STACK HEAP
βββββββββββββββ ββββββββββββββββββββ
β user: βββββββΌββββββββ>β { β
βββββββββββββββ β name: "Alice" β
β } β
ββββββββββββββββββββ
The variable user lives on the stack and holds a pointer (memory address). The actual object lives on the heap.
This is the key insight: When you copy a variable, you're copying the pointer, not the object itself.
The Five "Bugs" That Aren't Actually Bugs
Once you understand stack vs heap, these confusing behaviors suddenly make perfect sense.
1. The Accidental Mutation
const settings = { theme: "dark", language: "en" };
const userPrefs = settings; // Copies the POINTER, not the object
const adminPrefs = settings; // Another copy of the SAME pointer
userPrefs.theme = "light";
console.log(adminPrefs.theme); // "light" π±
What's happening in memory:
STACK HEAP
ββββββββββββββββββββ βββββββββββββββββββ
β settings: βββββΌβββ β { β
ββββββββββββββββββββ€ ββββββββ>β theme: "light"β
β userPrefs: βββββΌβββ€ β language: "en"β
ββββββββββββββββββββ€ β β } β
β adminPrefs: βββββΌβββ βββββββββββββββββββ
ββββββββββββββββββββ
All three variables point to the same object on the heap. When you change one, you change them all.
The fix:
const settings = { theme: "dark", language: "en" };
const userPrefs = { ...settings }; // NEW heap object
const adminPrefs = { ...settings }; // Another NEW heap object
userPrefs.theme = "light";
console.log(adminPrefs.theme); // "dark" β
Now each variable points to its own object.
2. The Array Surprise
function addItem(list, item) {
list.push(item); // Mutates the original heap object
return list;
}
const shoppingList = [1, 2, 3];
const newList = addItem(shoppingList, 4);
console.log(shoppingList); // [1, 2, 3, 4] π±
You returned the list, so it should be safe, right? Nope. push mutates the heap object that both variables point to.
The fix:
function addItem(list, item) {
return [...list, item]; // Creates NEW heap object
}
const shoppingList = [1, 2, 3];
const newList = addItem(shoppingList, 4);
console.log(shoppingList); // [1, 2, 3] β
console.log(newList); // [1, 2, 3, 4] β
3. The Closure That Remembers
This one blows people's minds:
function createCounter() {
let count = 0;
return function increment() {
count++;
return count;
};
}
const counter1 = createCounter();
const counter2 = createCounter();
console.log(counter1()); // 1
console.log(counter1()); // 2
console.log(counter2()); // 1 β Why doesn't this show 3?
Here's the beautiful part: When createCounter() finishes, normally everything on the stack gets cleared. But count is captured by the returned function, so JavaScript moves it to the heap.
After calling createCounter() twice:
HEAP
βββββββββββββββββββββββββββββββ
β Closure 1: { count: 2 } β β counter1 points here
βββββββββββββββββββββββββββββββ€
β Closure 2: { count: 1 } β β counter2 points here
βββββββββββββββββββββββββββββββ
Each function gets its own heap object with its own count. They don't interfere because they're pointing to different places in memory.
4. The const Confusion
const user = { name: "Alice" };
user = { name: "Bob" }; // β Error: Assignment to constant variable
user.name = "Bob"; // β
This works fine!
A lot of people think const means "immutable." It doesn't.
const means you can't reassign the pointer (on the stack). But you can absolutely mutate the object (on the heap).
STACK HEAP
ββββββββββ ββββββββββββββββ
β user βββΌβββββββ>β { name: ... }β β You CAN change this
ββββββββββ ββββββββββββββββ
β
βββ You CANNOT change this pointer
5. The React State Mystery
If you've ever wondered why this doesn't work in React:
// β This won't trigger a re-render
const [user, setUser] = useState({ name: "Alice" });
function updateName() {
user.name = "Bob"; // Mutates heap object
setUser(user); // Same pointer! React sees no change
}
// β
This works
function updateName() {
setUser({ ...user, name: "Bob" }); // NEW heap object, NEW pointer
}
React compares pointers, not object contents. If the pointer hasn't changed, React assumes nothing changed. This is why immutability is so important in React, you need to create new objects to trigger updates.
Try This Right Now
Open your browser console and run this experiment:
// Experiment 1: Primitives vs Objects
let a = 5;
let b = a;
b = 10;
console.log(a); // Predict: ___
const x = { value: 5 };
const y = x;
y.value = 10;
console.log(x.value); // Predict: ___
// Experiment 2: Arrays
const arr1 = [1, 2, 3];
const arr2 = arr1;
const arr3 = [...arr1];
arr2.push(4);
console.log(arr1); // Predict: ___
console.log(arr3); // Predict: ___
// Experiment 3: The Tricky One
const original = {
name: "Alice",
address: { city: "NYC" }
};
const copy = { ...original };
copy.address.city = "LA";
console.log(original.address.city); // Predict: ___
Answers:
-
5(primitives copy by value -aandbare completely separate) -
10(objects share the same heap reference -xandypoint to the same place) -
[1, 2, 3, 4]and[1, 2, 3](spread creates a new array, butarr2shares a reference witharr1) -
"LA"(gotcha! Spread only copies the top level; nested objects still share references)
That last one catches a lot of people. Spread operator does a shallow copy, not a deep copy.
When This Actually Matters: Performance
Understanding stack vs heap isn't just about preventing bugs. It can make your code significantly faster:
// π Slow: Creates 1 million heap objects
function processDataSlow(items) {
for (let i = 0; i < 1_000_000; i++) {
const temp = { index: i, value: items[i] };
doSomething(temp);
}
}
// β‘ Fast: Reuses one heap object
function processDataFast(items) {
const temp = { index: 0, value: null };
for (let i = 0; i < 1_000_000; i++) {
temp.index = i;
temp.value = items[i];
doSomething(temp);
}
}
In benchmarks, the fast version runs 3-5x faster because:
- Fewer garbage collection pauses
- Better CPU cache utilization
- Less memory allocation overhead
But here's the thing: Don't prematurely optimize. Use immutability by default (it prevents bugs), and only optimize hot paths when performance actually matters.
The Mental Model Cheat Sheet
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β ASSIGNMENT vs MUTATION β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β ASSIGNMENT (changes the pointer) β
β β Not allowed with const β
β β Creates new stack entry β
β β
β let x = { a: 1 }; β
β x = { a: 2 }; β Assignment β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β MUTATION (changes the heap object) β
β β Allowed with const β
β β Affects all references β
β β
β const x = { a: 1 }; β
β x.a = 2; β Mutation β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
How to Write Better Code Starting Tomorrow
1. Default to creating new objects
// Instead of mutating
user.name = "Bob";
// Create new objects
const updatedUser = { ...user, name: "Bob" };
2. Know which array methods mutate
// β These mutate the original array
push, pop, shift, unshift, splice, sort, reverse
// β
These return new arrays
concat, slice, map, filter, reduce, toSorted, toReversed
3. Be careful with nested objects
// Shallow copy doesn't protect nested objects!
const copy = { ...original };
// For deep copies:
const deepCopy = structuredClone(original); // Modern browsers
// or
const deepCopy = JSON.parse(JSON.stringify(original)); // Quick solution
4. Use TypeScript for better safety
// TypeScript can help catch mutations
interface User {
readonly name: string;
readonly role: string;
}
const user: User = { name: "Alice", role: "user" };
user.name = "Bob"; // β TypeScript error: Cannot assign to 'name'
5. Enable ESLint rules
// Add to your .eslintrc
{
"rules": {
"no-param-reassign": "error" // Warns about parameter mutations
}
}
Understanding Object Equality
Here's something that confuses developers all the time:
const a = { x: 1 };
const b = { x: 1 };
const c = a;
console.log(a === b); // false
console.log(a === c); // true
Why is a === b false when they look identical?
Both === and == compare pointers, not object contents. Even though a and b have the same properties and values, they're different objects on the heap. They occupy different memory locations.
Meanwhile, a === c is true because c = a copied the pointer. Both a and c point to the exact same heap object.
So how do you actually compare object contents?
For simple objects, you can use:
JSON.stringify(a) === JSON.stringify(b) // true
For complex objects with methods, dates, or circular references, use a library like Lodash:
import _ from 'lodash';
_.isEqual(a, b) // true
This understanding is crucial when working with React state, Redux reducers, or any code that depends on detecting changes in objects.
Common Misconceptions Cleared Up
"Primitives are always on the stack"
Not quite. Primitives in objects or closures live on the heap. JavaScript is smart about where to store things.
"Objects are slow because heap"
Modern engines optimize heavily. The performance difference is negligible for normal code. Don't micro-optimize based on this.
"const means the object is immutable"
Nope. const prevents reassignment of the variable, not mutation of the object it points to.
"Spread operator deep copies objects"
It's shallow only. Nested objects still share references.
Understanding stack vs heap isn't about memorizing rules. It's about building an accurate mental model of what JavaScript is actually doing when your code runs.
Once you have that model, the "bugs" aren't mysterious anymore. They're predictable. And predictable means fixable.
Top comments (0)