JavaScript Interview Prep

Value vs Reference in JavaScript: The Mental Model That Never Breaks

🎯 Interview-Ready Deep Dive — Why does changing one object change another? Understanding JavaScript's memory model (Stack vs Heap) is the key to bug-free code.

Value vs Reference in JavaScript: The Mental Model That Never Breaks

🎯 Interview-Ready Deep Dive — This isn’t a surface-level overview. I break down exactly how JavaScript handles memory, why certain behaviors occur, and what interviewers expect you to know. Master this, and you’ll confidently answer any question on pass-by-value, object mutation, and reference behavior.

TL;DR: JavaScript is always pass-by-value. For primitives (string, number, boolean, etc.), the value is the actual data. For objects (arrays, functions, objects), the value is a reference to the data. Mutations propagate through all references, but reassignments do not.


Quick Reference

ConceptBehavior
PrimitivesImmutable; copied by value
ObjectsMutable by default; variables store references
MutationChanges the object (propagates to all references)
ReassignmentReplaces the reference (doesn’t propagate)
Shallow CopyOnly top-level is copied; nested objects are shared
Deep CopyAll levels are duplicated; no shared state

Table of Contents


The One Rule You Must Remember

JavaScript is always pass-by-value.

There are no exceptions.

What confuses developers is what the value actually is:

  • For primitives → the value is the data itself
  • For objects → the value is a reference to the data

Everything else follows from this foundational concept.


Two Categories of Data in JavaScript

Primitives (Value Types)

JavaScript has seven primitive types: string, number, bigint, boolean, undefined, symbol, and null.

Key properties:

  • ✅ Immutable — cannot be changed after creation
  • ✅ Copied by value — assignments create independent copies
  • ✅ No shared state — changing one variable never affects another
let a = 10;
let b = a;

b = 20;

console.log(a); // 10
console.log(b); // 20

Objects (Reference Types)

Everything that isn’t a primitive is an object: {}, [], functions, Date, Map, Set, class instances.

const obj1 = { count: 1 };
const obj2 = obj1;

obj2.count = 2;

console.log(obj1.count); // 2

Both variables point to the same object in memory — mutating it affects all references.


How Memory Works: Visualized

Understanding where data lives in memory is the key to mastering this concept.

Primitives: Each Variable Gets Its Own Copy

flowchart LR
    subgraph Stack["Stack Memory"]
        A["a = 10"]
        B["b = 10"]
    end
    
    style A fill:#4ade80,stroke:#166534,color:#000
    style B fill:#4ade80,stroke:#166534,color:#000

When you assign let b = a, the value 10 is copied. Each variable holds its own independent value.

let a = 10;
let b = a;    // b gets a COPY of 10
b = 20;       // Only b changes
// a is still 10

Objects: Variables Share a Reference

flowchart LR
    subgraph Stack["Stack Memory"]
        A["obj1 → ref#1"]
        B["obj2 → ref#1"]
    end
    
    subgraph Heap["Heap Memory"]
        OBJ["{ count: 1 }"]
    end
    
    A --> OBJ
    B --> OBJ
    
    style A fill:#60a5fa,stroke:#1e40af,color:#000
    style B fill:#60a5fa,stroke:#1e40af,color:#000
    style OBJ fill:#fbbf24,stroke:#b45309,color:#000

ℹ️ These diagrams represent a conceptual model; actual JavaScript engines may optimize memory differently.

When you assign const obj2 = obj1, only the reference is copied — both variables point to the same object.

const obj1 = { count: 1 };
const obj2 = obj1;  // obj2 gets a COPY of the reference
obj2.count = 2;     // Mutates the shared object
// obj1.count is now 2

Reassignment vs Mutation (Critical Distinction)

This is where most confusion happens. Let’s visualize both scenarios.

Reassignment Creates a New Reference

flowchart TB
    subgraph Before["Before Reassignment"]
        direction LR
        A1["a → ref#1"] --> OBJ1["{ x: 1 }"]
        B1["b → ref#1"] --> OBJ1
    end
    
    Before --> After
    
    subgraph After["After: b = { x: 2 }"]
        direction LR
        A2["a → ref#1"] --> OBJ2["{ x: 1 }"]
        B2["b → ref#2"] --> OBJ3["{ x: 2 }"]
    end
    
    style OBJ1 fill:#fbbf24,stroke:#b45309,color:#000
    style OBJ2 fill:#fbbf24,stroke:#b45309,color:#000
    style OBJ3 fill:#f87171,stroke:#b91c1c,color:#000
let a = { x: 1 };
let b = a;

b = { x: 2 };  // Reassignment — b points to NEW object

console.log(a.x); // 1  ← unchanged
console.log(b.x); // 2

Mutation Modifies the Shared Object

flowchart TB
    subgraph Before["Before Mutation"]
        direction LR
        A1["a → ref#1"] --> OBJ1["{ x: 1 }"]
        B1["b → ref#1"] --> OBJ1
    end
    
    Before --> After
    
    subgraph After["After: b.x = 2"]
        direction LR
        A2["a → ref#1"] --> OBJ2["{ x: 2 }"]
        B2["b → ref#1"] --> OBJ2
    end
    
    style OBJ1 fill:#fbbf24,stroke:#b45309,color:#000
    style OBJ2 fill:#f87171,stroke:#b91c1c,color:#000
let a = { x: 1 };
let b = a;

b.x = 2;  // Mutation — modifies the SAME object

console.log(a.x); // 2  ← changed!
console.log(b.x); // 2

Function Arguments: The Most Misunderstood Part

JavaScript uses call-by-sharing — the function receives a copy of the reference, not the reference itself.

Mutation Persists Outside the Function

function update(user) {
  user.name = "Updated";
}

const account = { name: "Initial" };
update(account);

console.log(account.name); // "Updated"

The function mutates the same object that account references.

Reassignment Stays Local

function replace(user) {
  user = { name: "New" };  // Local reassignment only
}

const account = { name: "Initial" };
replace(account);

console.log(account.name); // "Initial"  ← unchanged

The local parameter user was pointed to a new object, but account still references the original.

flowchart TB
    subgraph Caller["Caller Scope"]
        ACC["account → ref#1"]
    end
    
    subgraph Function["Function Scope"]
        PARAM["user → ref#1"]
        PARAM2["user → ref#2"]
    end
    
    subgraph Heap["Heap Memory"]
        OBJ1["{ name: 'Initial' }"]
        OBJ2["{ name: 'New' }"]
    end
    
    ACC --> OBJ1
    PARAM -.->|"before"| OBJ1
    PARAM2 -->|"after reassignment"| OBJ2
    
    style OBJ1 fill:#4ade80,stroke:#166534,color:#000
    style OBJ2 fill:#f87171,stroke:#b91c1c,color:#000

Object Comparison: Identity Matters

{} === {}   // false
[] === []   // false

Each literal creates a new object with a unique reference. JavaScript compares objects by identity (reference), not by structure.

const a = {};
const b = a;

a === b; // true  ← same reference

Copying Objects: Shallow vs Deep

Shallow Copy (Spread Operator)

const original = { nested: { value: 1 } };
const copy = { ...original };

copy.nested.value = 99;

console.log(original.nested.value); // 99  ← also changed!
flowchart LR
    subgraph Variables
        ORIG["original"]
        COPY["copy"]
    end
    
    subgraph TopLevel["Top Level (Copied)"]
        O1["{ nested: → }"]
        O2["{ nested: → }"]
    end
    
    subgraph Nested["Nested (Shared!)"]
        N1["{ value: 1 }"]
    end
    
    ORIG --> O1
    COPY --> O2
    O1 --> N1
    O2 --> N1
    
    style N1 fill:#f87171,stroke:#b91c1c,color:#000

Deep Copy (structuredClone)

⚠️ Availability note:
Use structuredClone() for deep copies. It is available in modern browsers and Node.js v17+.

const original = { nested: { value: 1 } };
const copy = structuredClone(original);

copy.nested.value = 99;

console.log(original.nested.value); // 1  ← unchanged
flowchart LR
    subgraph Variables
        ORIG["original"]
        COPY["copy"]
    end
    
    subgraph Original_Tree["Original Tree"]
        O1["{ nested: → }"]
        N1["{ value: 1 }"]
    end
    
    subgraph Cloned_Tree["Cloned Tree"]
        O2["{ nested: → }"]
        N2["{ value: 1 }"]
    end
    
    ORIG --> O1
    O1 --> N1
    COPY --> O2
    O2 --> N2
    
    style N1 fill:#4ade80,stroke:#166534,color:#000
    style N2 fill:#4ade80,stroke:#166534,color:#000

Common Bugs Caused by Misunderstanding References

Bug 1: Mutating Shared Default Values

// ❌ BAD
const defaultConfig = { timeout: 5000 };

function setup(config = defaultConfig) {
  config.timeout = 10000;  // Mutates defaultConfig globally!
}

// ✅ FIX
function setup(config = {}) {
  const local = { ...defaultConfig, ...config };
}

Bug 2: Assuming Spread Is a Deep Copy

// ❌ BAD
const copy = { ...obj };
copy.nested.value = 1;  // Mutates original!

// ✅ FIX
const copy = structuredClone(obj);

How to Prevent Accidental Mutations

Defensive Deep Copy

function process(data) {
  const local = structuredClone(data);
  // Safe to modify
  return local;
}

Object.freeze (Shallow Only)

const config = Object.freeze({ timeout: 5000 });
config.timeout = 10000;  // Silently fails (throws `TypeError` in strict mode)

⚠️ Object.freeze() is shallow — nested objects must be frozen separately.


The Correct Mental Model (Summary)

flowchart TD
    START["Variable Assignment"] --> CHECK{"What type?"}
    
    CHECK -->|"Primitive"| PRIM["Value is COPIED"]
    CHECK -->|"Object"| OBJ["Reference is COPIED"]
    
    PRIM --> PRIM_RESULT["Independent copies<br/>No shared state"]
    OBJ --> OBJ_CHECK{"What operation?"}
    
    OBJ_CHECK -->|"Mutation"| MUT["Changes propagate<br/>to all references"]
    OBJ_CHECK -->|"Reassignment"| REASS["Only local variable<br/>is affected"]
    
    style PRIM fill:#4ade80,stroke:#166534,color:#000
    style MUT fill:#f87171,stroke:#b91c1c,color:#000
    style REASS fill:#60a5fa,stroke:#1e40af,color:#000

Remember:

  • Primitives → copied by value
  • Objects → referenced by value
  • Mutations → propagate
  • Reassignments → don’t propagate
  • JavaScript → always pass-by-value

Interview Self-Check

Test your understanding — try answering each question before revealing the answer.

1. Why does {} !== {} return true?

Reveal Answer

Each {} literal creates a new object in memory with its own unique reference. JavaScript compares objects by identity (reference), not by structure. Since these are two different objects at two different memory addresses, they are not equal.

const a = {};
const b = {};
a === b  // false — different references

const c = a;
a === c  // true — same reference

2. Why does mutation escape function scope but reassignment doesn’t?

Reveal Answer

When you pass an object to a function, the function receives a copy of the reference — not the object itself, and not the original reference variable.

  • Mutation modifies the object that both references point to → changes persist
  • Reassignment only changes what the local copy points to → caller’s variable is unaffected
function mutate(obj) { obj.x = 2; }      // Modifies shared object ✓
function reassign(obj) { obj = {x: 2}; } // Only changes local copy ✗

3. Why does the spread operator only create a shallow copy?

Reveal Answer

The spread operator {...obj} copies each property’s value into a new object. For primitive properties, this creates independent copies. But for nested objects, the “value” being copied is a reference — so both the original and the copy point to the same nested object.

const original = { nested: { x: 1 } };
const copy = { ...original };

// copy.nested === original.nested (same reference!)

Use structuredClone() for deep copies.

4. Why is JavaScript not pass-by-reference?

Reveal Answer

In true pass-by-reference (like C++ with &), the function receives the actual variable from the caller — reassigning it would change the caller’s variable.

JavaScript is pass-by-value. For objects, the value passed is a reference, but it’s a copy of that reference. Reassigning the parameter creates a new local binding; it cannot affect the caller’s variable.

function tryToReplace(arr) {
  arr = [1, 2, 3];  // Only reassigns local parameter
}

const myArray = [];
tryToReplace(myArray);
console.log(myArray);  // [] — unchanged (this wouldn't happen with true pass-by-reference)

5. Why can two variables affect the same object?

Reveal Answer

Variables don’t store objects — they store references to objects. When you assign one variable to another (const b = a), you copy the reference, not the object. Both variables now point to the same object in memory, so mutating through either variable affects the shared object.

const a = { count: 0 };
const b = a;  // b receives a copy of the reference

b.count++;
console.log(a.count);  // 1 — same object

Closing Thoughts

This concept is foundational. Closures, immutability, state management, performance optimization, and Node.js memory behavior all depend on it.

If your mental model here is correct, everything else builds cleanly.


Further Reading