Value vs Reference
Aliasing, shallow copy, mutation through a handle.
Value vs Reference
When you assign a variable or pass an argument, what actually gets copied? The answer depends on the type, and getting it wrong is the source of some of the worst mystery bugs in this job.
Analogy
Think of sharing information with a friend. Telling them your birthday is copying by value — if they scribble over their note, yours is untouched. Giving them your home address is sharing a reference — two people now know the way to the same house, and if one of them rearranges the furniture, the other walks in to find the sofa gone. Worse: writing the address on a sticky note and handing them the note means you can still wander over yourself, but if they burn the note you're not sure whose house gets touched next. That last flavour — "the note, not the house" — is where mystery bugs hide.
Primitives are copied by value
In JavaScript and Python, primitives — numbers, booleans, strings, Symbols — are copied by value. There's only ever one copy of the value being passed around, so mutation is impossible. Strings in both languages are immutable: s.replace(...) returns a new string, it doesn't edit the original.
let a = 5;
let b = a; // b is a copy of a's value
b = 10;
console.log(a); // 5 — untouched
Objects are shared by reference
Objects, arrays, maps, sets, class instances — anything on the heap — are passed by reference. The variable doesn't hold the object; it holds a pointer to the object on the heap. Copying the variable copies the pointer, not the object:
const a = { count: 0 };
const b = a; // b and a point to the SAME object
b.count = 99;
console.log(a.count); // 99 — same heap object
This is the aliasing problem. Any number of names can point to the same object, and mutation through any one of them is visible to all of them.
Pass-by-reference in functions
Passing an object to a function gives the function the same reference. The parameter is a local name, but the object it points to is shared:
function addTag(user) {
user.tags.push("vip");
}
const alice = { tags: ["new"] };
addTag(alice);
console.log(alice.tags); // ["new", "vip"] — mutated!
This is the #1 source of "how did this field change?" bugs. Rule of thumb: if a function takes an object and doesn't return a new one, assume it might mutate the argument.
Shallow vs deep copy
{ ...obj }, Object.assign({}, obj), arr.slice(), and Array.from(arr) are all shallow copies. They duplicate the top-level container, but nested references are shared:
const a = { name: "ada", tags: ["new"] };
const b = { ...a };
b.tags.push("vip");
console.log(a.tags); // ["new", "vip"] — same array!
For true independence, use a deep copy:
const c = structuredClone(a); // deep copy, handles Date, Map, Set, circular refs
In Python: copy.deepcopy(a).
Avoid JSON.parse(JSON.stringify(a)) — it silently drops functions, undefined, Date (becomes a string), Map, Set, and dies on circular references.
Mutable default arguments — the Python classic
def add(x, to=[]):
to.append(x)
return to
add(1) # [1]
add(2) # [1, 2] — the list persists across calls!
The to=[] default is evaluated once, when the function is defined. Every call reuses the same list. The idiomatic fix:
def add(x, to=None):
if to is None:
to = []
to.append(x)
return to
Why immutability helps
If your values can't be mutated, aliasing is safe. This is the pitch for:
- Immutable data (Immer, Immutable.js, Record,
readonlytypes,const). - Functional updates that return a new object:
{ ...state, count: state.count + 1 }. - Frozen objects (
Object.freeze) — though shallow, so they only block top-level mutation.
Languages like Clojure, Elm, and Rust (via the borrow checker) make this the default. In JS and Python, you have to opt in.
The mental model
Every variable is a name that points to a value. Primitives live in the name. Objects live on the heap, and the name holds a pointer. Copying the name copies the pointer, not the heap object.
When in doubt: draw boxes and arrows. Every variable is a box. Every arrow points to a heap object. Mutation changes what's inside an object; reassignment just bends an arrow.