datatypes · level 3

Value vs Reference

Aliasing, shallow copy, mutation through a handle.

225 XP

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, readonly types, 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.