Skip to content
JavaScript js fundamentals 4 min read

Primitives vs References

One of the most consequential rules in JavaScript is how values get stored and copied. Primitives are copied by value, while objects (including arrays and functions) are copied by reference. Misunderstanding this single distinction is behind a surprising number of bugs — variables that “change on their own,” function arguments that mutate the caller’s data, and equality checks that fail when you expect them to pass.

The two categories of values

JavaScript has seven primitive types: string, number, boolean, null, undefined, symbol, and bigint. Everything else — objects, arrays, functions, dates, maps, sets — is a reference type.

A primitive is an immutable, self-contained value. When you assign or pass it around, the engine copies the actual value. A reference type lives somewhere in memory, and the variable only holds a pointer (a reference) to that location. Assigning it copies the pointer, not the underlying object.

Stack vs heap

A useful mental model: primitives sit directly on the stack alongside the variable, while objects live on the heap and the variable holds a reference into it.

let a = 10;            STACK                    HEAP
let user = { x: 1 };   ┌──────────────┐         ┌──────────────┐
                       │ a    │ 10    │         │ { x: 1 }     │
                       │ user │ ──────┼────────▶│ @0x1f3       │
                       └──────────────┘         └──────────────┘

When you copy a, you copy the number 10. When you copy user, you copy the arrow (@0x1f3) — both variables now point at the same object on the heap.

Assignment: value vs reference

With primitives, the copy is fully independent. Changing one variable never affects the other.

let a = 10;
let b = a;   // b gets its own copy of 10
b = 20;

console.log(a, b);

Output:

10 20

With objects, both variables point at the same thing, so a mutation through one is visible through the other.

const original = { count: 1 };
const alias = original;   // copies the reference, not the object
alias.count = 99;

console.log(original.count);

Output:

99

Gotcha: const only freezes the binding, not the object. const obj = {} means obj can never be reassigned, but obj.key = 'value' is perfectly legal. To prevent mutation, use Object.freeze().

Passing to functions

JavaScript is always pass-by-value — but for objects, the value being passed is the reference. So a function can mutate the object its argument points to, while it can never change a primitive the caller holds.

function tryToChange(num, obj) {
  num = 0;          // local copy only
  obj.changed = true; // mutates the shared object
}

let score = 100;
const state = { changed: false };
tryToChange(score, state);

console.log(score, state.changed);

Output:

100 true

Reassigning the parameter (obj = {}) inside the function would not affect the caller — you’d only be repointing the local copy of the reference.

Comparison with ===

Strict equality compares primitives by their value, but objects by their reference identity. Two distinct objects with identical contents are never equal.

console.log("hi" === "hi");        // same value
console.log(1 === 1.0);            // same value
console.log({} === {});            // different references
console.log([1] === [1]);          // different references

const ref = { a: 1 };
console.log(ref === ref);          // same reference

Output:

true
true
false
false
true
OperationPrimitivesReferences
b = acopies the valuecopies the pointer
Pass to functionindependent copyshared object
a === bcompares valuescompares identity
Mutate via aliasimpossibleaffects all aliases

Shallow vs deep copy

Because assignment only copies a reference, you often need an explicit copy. A shallow copy duplicates the top level but shares any nested objects. The spread operator and Object.assign() both produce shallow copies.

const settings = { theme: "dark", layout: { sidebar: true } };
const copy = { ...settings };

copy.theme = "light";          // independent — top level
copy.layout.sidebar = false;   // SHARED — nested object

console.log(settings.theme, settings.layout.sidebar);

Output:

dark false

A deep copy clones every level. The modern, built-in tool is structuredClone() (available in browsers and Node 17+), which handles nested objects, arrays, Maps, Sets, and circular references.

const settings = { theme: "dark", layout: { sidebar: true } };
const deep = structuredClone(settings);

deep.layout.sidebar = false;   // fully independent now

console.log(settings.layout.sidebar, deep.layout.sidebar);

Output:

true false

Tip: Avoid the old JSON.parse(JSON.stringify(obj)) trick for deep copies — it silently drops undefined, functions, and symbols, and turns Date objects into strings. Prefer structuredClone().

Best Practices

  • Treat primitives as values and objects as shared pointers — let that guide every copy and equality decision.
  • Use the spread operator for cheap, immutable-style updates of flat objects and arrays.
  • Reach for structuredClone() when you need a true deep copy of nested data.
  • Remember const prevents reassignment, not mutation; use Object.freeze() to lock contents.
  • Never compare objects with === to test contents — compare specific fields or serialize intentionally.
  • In functions, avoid mutating object arguments unless the caller expects it; return a new object instead.
  • Be aware that shallow copies still share nested references — a frequent source of “spooky action at a distance.”
Last updated June 1, 2026
Was this helpful?