Here's the moment JavaScript stopped making sense to me for an afternoon.
let a = 10;
let b = a;
b = 20;
console.log(a); // 10 — fine, a didn't change
Makes sense. b got its own copy of 10, I changed b, and a stayed put. Exactly what you'd expect.
Then I did what looked like the same thing with an object:
let person1 = { name: "Parth" };
let person2 = person1;
person2.name = "Vatsal";
console.log(person1.name); // "Vatsal" — wait, WHAT?
I changed person2. But person1 changed too. I never touched person1.
Same pattern, completely different result. Figuring out why taught me one of the most important ideas in the language: some values are copied, and some are shared. Which one you get depends on the type — and it comes down to where JavaScript stores things.
Two kinds of data, two kinds of storage
From an earlier post in this series: JavaScript has two families of types.
- Primitives — string, number, boolean, null, undefined, bigint, symbol. A single value.
- Non-primitives — objects, arrays, functions. Collections of values. What I didn't know is that JavaScript keeps them in two different places in memory.
The stack is for primitives. It's small, fast, and orderly, and it holds the value directly. When a variable holds a number, the number itself is sitting right there in the stack.
The heap is for objects. Objects can be big and grow over time, so they live in a larger, more flexible space. But the variable doesn't hold the object itself — it holds a reference: an address that points to where the object lives in the heap.
That difference — value vs address — is the whole explanation.
Why the number behaved
let a = 10;
let b = a;
a holds 10 directly in the stack. When you write let b = a, JavaScript copies the value. Now b has its own 10, completely separate.
Change b, and a has no idea. They were never connected — just two boxes that happened to hold the same number.
This is called copy by value.
Why the object didn't
let person1 = { name: "Parth" };
let person2 = person1;
person1 doesn't hold the object. It holds a reference — an address pointing to the object in the heap.
When you write let person2 = person1, JavaScript copies the reference, not the object. So now person1 and person2 hold the same address. Two variables, one object, pointing at the exact same thing in the heap.
So when you do person2.name = "Vatsal", you follow the address to the one object and change it. And since person1 points to that same object, it sees the change too.
You didn't have two objects. You had two signposts pointing at one.
This is copy by reference.
The mental picture that made it click
A primitive is like writing a phone number on two separate sticky notes. Change one note, the other is untouched — they were only ever copies.
An object is like two sticky notes that both say "the contact is in drawer 3." Go to drawer 3 and change the contact, and both notes now point to the changed thing. You didn't copy the contact. You copied the location.
STACK (primitives, held directly) HEAP (objects, held by reference)
a → 10 person1 ──┐
b → 10 (its own copy) ├──→ { name: "Vatsal" }
person2 ──┘ (one object, two pointers)
This connects back to const
In the var/let/const post, I mentioned a const quirk that now makes complete sense:
const person = { name: "Parth" };
person.name = "Vatsal"; // works fine
person = { name: "Aryan" }; // TypeError
const locks the reference — the address the variable holds. You can't point it at a different object. But you can absolutely follow the existing address and change what's inside. The box is glued in place; its contents aren't.
Stack-vs-heap is why. const protects what's in the variable (the address), not what's across the heap at the other end of it.
The practical catch
This bites people in real code constantly. The classic version:
let original = { count: 0 };
let backup = original; // you THINK you saved a copy
original.count = 99;
console.log(backup.count); // 99 — your "backup" changed too
backup was never a copy. It's the same object. If you actually want a separate one, you have to make a real copy — for a shallow one:
let backup = { ...original }; // spread into a brand new object
Now backup points to its own object in the heap, and the two move independently.
What I actually took away
I used to think "copy" meant one thing. It doesn't. In JavaScript, copying a primitive copies the value, and copying an object copies the address — and those two behave in opposite ways the moment you change something.
Once I pictured the stack holding values and the heap holding objects behind pointers, every one of these "wait, what?" moments turned into "oh, of course." The number stays independent because it was copied. The object moves together because it was shared.
Same word, two behaviors. The type tells you which one you're getting.
Did this one get you too? The backup that wasn't actually a backup got me early on. I'd love to hear your version in the comments.
Top comments (0)