Have you ever wondered why changing one variable unexpectedly changes another? Why does this happen?
const original = { name: "Alice" };
const copy = original;
copy.name = "Bob";
console.log(original.name); // "Bob" — Wait, what?!The answer lies in how JavaScript values behave — not where they're stored. Primitives are immutable and behave independently, while objects are mutable and can be shared between variables.
Myth vs Reality: You may have heard that "primitives are stored on the stack" and "objects are stored on the heap," or that "primitives are passed by value" while "objects are passed by reference." These are simplifications that are technically incorrect. In this guide, we'll learn how JavaScript actually works.
What you'll learn in this guide:
- The real difference between primitives and objects (it's about mutability, not storage)
- Why JavaScript uses "call by sharing" — not "pass by value" or "pass by reference"
- Why mutation works through function parameters but reassignment doesn't
- Why
{} === {}returnsfalse(object identity) - How to properly clone objects (shallow vs deep copy)
- Common bugs caused by shared references
- Bonus: How V8 actually stores values in memory (the technical truth)
Prerequisite: This guide assumes you understand Primitive Types. If you're not familiar with the 7 primitive types in JavaScript, read that guide first!
A Note on Terminology
Before we dive in, let's clear up some widespread misconceptions that even experienced developers get wrong.
Myth vs Reality
| Common Myth | The Reality |
|---|---|
| "Value types" vs "reference types" | ECMAScript only defines primitives and objects |
| "Primitives are stored on the stack" | Implementation-specific — not in the spec |
| "Objects are stored on the heap" | Implementation-specific — not in the spec |
| "Primitives are passed by value" | JavaScript uses call by sharing for ALL values |
| "Objects are passed by reference" | Objects are passed by sharing (you can't reassign the original) |
What ECMAScript Actually Says
The ECMAScript specification (the official JavaScript standard) defines exactly two categories of values. According to the 2023 State of JS survey, confusion around value vs reference behavior remains one of the most common pain points for developers learning JavaScript:
| ECMAScript Term | What It Includes |
|---|---|
| Primitive values | string, number, bigint, boolean, undefined, null, symbol |
| Objects | Everything else (plain objects, arrays, functions, dates, maps, sets, etc.) |
That's it. The spec never mentions "value types," "reference types," "stack," or "heap." These are implementation details that vary by JavaScript engine.
The Real Distinction: Mutability
The fundamental difference between primitives and objects is mutability:
- Primitives are immutable — you cannot change a primitive value, only replace it
- Objects are mutable — you CAN change an object's contents
This distinction explains ALL the behavioral differences you'll encounter.
How Primitives and Objects Behave
Primitives: Immutable and Independent
The 7 primitive types behave as if each variable has its own independent copy:
| Type | Example | Key Behavior |
|---|---|---|
string | "hello" | Immutable — methods return NEW strings |
number | 42 | Immutable — arithmetic creates NEW numbers |
bigint | 9007199254740993n | Immutable — operations create NEW BigInts |
boolean | true | Immutable |
undefined | undefined | Immutable |
null | null | Immutable |
symbol | Symbol("id") | Immutable AND has identity |
Key characteristics:
- Immutable — you can't change them, only replace them
- Behave independently — copies don't affect each other
- Compared by value — same value = equal (except Symbols)
Why immutability matters: When you write str.toUpperCase(), you get a NEW string. The original str is unchanged. This is true for ALL string methods — they never mutate the original string.
let greeting = "hello";
let shout = greeting.toUpperCase();
console.log(greeting); // "hello" — unchanged!
console.log(shout); // "HELLO" — new stringObjects: Mutable and Shared
Everything that's not a primitive is an object:
| Type | Example | Key Behavior |
|---|---|---|
| Object | { name: "Alice" } | Mutable — properties can change |
| Array | [1, 2, 3] | Mutable — elements can change |
| Function | function() {} | Mutable (has properties) |
| Date | new Date() | Mutable |
| Map | new Map() | Mutable |
| Set | new Set() | Mutable |
Key characteristics:
- Mutable — you CAN change their contents
- Shared by default — assignment copies the reference, not the object
- Compared by identity — same object = equal (not same contents!)
The House Key Analogy
Think of objects like houses and variables like keys to those houses:
Primitives (like writing a note): You write "42" on a sticky note and give a copy to your friend. You each have independent notes. If they change theirs to "100", your note still says "42".
Objects (like sharing house keys): Instead of giving your friend the house itself, you give them a copy of your house key. You both have keys to the SAME house. If they rearrange the furniture, you'll see it too — because it's the same house!
┌─────────────────────────────────────────────────────────────────────────┐
│ PRIMITIVES vs OBJECTS: THE KEY ANALOGY │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ PRIMITIVES (Independent Notes) OBJECTS (Keys to Same House) │
│ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ a = "42" │ │ x = 🔑 ─────────────┐ │
│ └─────────────┘ └─────────────┘ │ │
│ ▼ │
│ ┌─────────────┐ ┌─────────────┐ ┌──────────┐ │
│ │ b = "42" │ (separate copy) │ y = 🔑 ─────────►│ 🏠 │ │
│ └─────────────┘ └─────────────┘ │ {name} │ │
│ └──────────┘ │
│ Change b to "100"? Change the house via y? │
│ a stays "42"! x sees the change too! │
│ │
└─────────────────────────────────────────────────────────────────────────┘The key insight: it's not about where the key is stored, it's about what it points to.
Call by Sharing: How JavaScript Passes Arguments
Here's where most tutorials get it wrong. JavaScript doesn't use "pass by value" OR "pass by reference." It uses a third strategy called call by sharing (also known as "call by object sharing").
Call by sharing was first described by Barbara Liskov for the CLU programming language in 1974. JavaScript, Python, Ruby, and Java all use this evaluation strategy.
What is Call by Sharing?
When you pass an argument to a function, JavaScript:
- Creates a copy of the reference (the "key" to the object)
- The function parameter gets this copied reference
- Both the original variable AND the parameter point to the SAME object
The Golden Rule
| Operation | Does it affect the original? |
|---|---|
Mutating properties (obj.name = "Bob") | ✅ Yes — same object |
Reassigning the parameter (obj = newValue) | ❌ No — only rebinds locally |
Mutation Works
When you modify an object through a function parameter, the original object is affected:
function rename(person) {
person.name = "Bob"; // Mutates the ORIGINAL object
}
const user = { name: "Alice" };
rename(user);
console.log(user.name); // "Bob" — changed!What happens in memory:
BEFORE rename(user): INSIDE rename(user):
┌────────────┐ ┌────────────┐
│user = 🔑 ──┼──► { name: │user = 🔑 ──┼──► { name: "Bob" }
└────────────┘ "Alice" } ├────────────┤ ▲
│person= 🔑 ─┼───────┘
└────────────┘ (same house!)Reassignment Doesn't Work
If you reassign the parameter to a new object, it only changes the local variable:
function replace(person) {
person = { name: "Charlie" }; // Creates NEW local reference
}
const user = { name: "Alice" };
replace(user);
console.log(user.name); // "Alice" — unchanged!What happens in memory:
INSIDE replace(user):
┌────────────┐ ┌─────────────────┐
│user = 🔑 ──┼───►│ { name: "Alice" }│ ← Original, unchanged
├────────────┤ └─────────────────┘
│person= 🔑 ─┼───►┌───────────────────┐
└────────────┘ │ { name: "Charlie" }│ ← New object, local only
└───────────────────┘Why this matters: If JavaScript used true "pass by reference" (like C++ references), reassigning the parameter WOULD change the original. It doesn't in JavaScript — that's how you know it's "call by sharing," not "pass by reference."
This Applies to Primitives Too!
Here's the mind-bending part: primitives are also passed by sharing. You just can't observe it because primitives are immutable — there's no way to mutate them through the parameter.
function double(num) {
num = num * 2; // Reassigns the LOCAL variable
return num;
}
let x = 10;
let result = double(x);
console.log(x); // 10 — unchanged (reassignment doesn't affect original)
console.log(result); // 20 — returned valueThe same "reassignment doesn't work" rule applies to primitives. It's just that with primitives, there's no mutation to try anyway!
Copying Behavior: The Critical Difference
This is where bugs love to hide.
Copying Primitives: Independent Copies
When you copy a primitive, they behave as completely independent values:
let a = 10;
let b = a; // b gets an independent copy
b = 20; // changing b has NO effect on a
console.log(a); // 10 (unchanged!)
console.log(b); // 20Copying Objects: Shared References
When you copy an object variable, you copy the reference. Both variables now point to the SAME object:
let obj1 = { name: "Alice" };
let obj2 = obj1; // obj2 gets a copy of the REFERENCE
obj2.name = "Bob"; // modifies the SAME object!
console.log(obj1.name); // "Bob" (changed!)
console.log(obj2.name); // "Bob"The Array Gotcha
Arrays are objects too, so they behave the same way:
let arr1 = [1, 2, 3];
let arr2 = arr1; // arr2 points to the SAME array
arr2.push(4); // modifies the shared array
console.log(arr1); // [1, 2, 3, 4] — Wait, what?!
console.log(arr2); // [1, 2, 3, 4]This trips up EVERYONE at first! When you write let arr2 = arr1, you're NOT creating a new array. You're creating a second variable that points to the same array. Any changes through either variable affect both.
Comparison Behavior
Primitives: Compared by Value
Two primitives are equal if they have the same value:
let a = "hello";
let b = "hello";
console.log(a === b); // true — same value
let x = 42;
let y = 42;
console.log(x === y); // true — same valueObjects: Compared by Identity
Two objects are equal only if they are the SAME object (same reference):
let obj1 = { name: "Alice" };
let obj2 = { name: "Alice" };
console.log(obj1 === obj2); // false — different objects!
let obj3 = obj1;
console.log(obj1 === obj3); // true — same referenceThe Empty Object/Array Trap
console.log({} === {}); // false — two different empty objects
console.log([] === []); // false — two different empty arrays
console.log([1,2] === [1,2]); // false — two different arraysHow to compare objects/arrays by content:
// Simple (but limited) approach
JSON.stringify(obj1) === JSON.stringify(obj2)
// For arrays of primitives
arr1.length === arr2.length && arr1.every((v, i) => v === arr2[i])
// For complex cases, use a library like Lodash
_.isEqual(obj1, obj2)Caution with JSON.stringify: Property order matters! {a:1, b:2} and {b:2, a:1} produce different strings. It also fails with undefined, functions, Symbols, circular references, NaN, and Infinity.
Symbols: The Exception
Symbols are primitives but have identity — two symbols with the same description are NOT equal:
const sym1 = Symbol("id");
const sym2 = Symbol("id");
console.log(sym1 === sym2); // false — different symbols!
console.log(sym1 === sym1); // true — same symbolMutation vs Reassignment
Understanding this distinction is crucial for avoiding bugs.
Mutation: Changing the Contents
Mutation modifies the existing object in place:
const arr = [1, 2, 3];
// These are all MUTATIONS:
arr.push(4); // [1, 2, 3, 4]
arr[0] = 99; // [99, 2, 3, 4]
arr.pop(); // [99, 2, 3]
arr.sort(); // modifies in place
const obj = { name: "Alice" };
// These are all MUTATIONS:
obj.name = "Bob"; // changes property
obj.age = 25; // adds property
delete obj.age; // removes propertyReassignment: Pointing to a New Value
Reassignment makes the variable point to something else entirely:
let arr = [1, 2, 3];
arr = [4, 5, 6]; // REASSIGNMENT — new array
let obj = { name: "Alice" };
obj = { name: "Bob" }; // REASSIGNMENT — new objectThe const Trap
const prevents reassignment but NOT mutation:
const arr = [1, 2, 3];
// ✅ Mutations are ALLOWED:
arr.push(4); // works!
arr[0] = 99; // works!
// ❌ Reassignment is BLOCKED:
arr = [4, 5, 6]; // TypeError: Assignment to constant variable
const obj = { name: "Alice" };
// ✅ Mutations are ALLOWED:
obj.name = "Bob"; // works!
obj.age = 25; // works!
// ❌ Reassignment is BLOCKED:
obj = { name: "Eve" }; // TypeError: Assignment to constant variableCommon misconception: Many developers think const creates an "immutable" variable. It doesn't! It only prevents reassignment. The contents of objects and arrays declared with const can still be changed.
True Immutability with Object.freeze()
If you need a truly immutable object, use Object.freeze():
const user = Object.freeze({ name: "Alice", age: 25 });
user.name = "Bob"; // Silently fails (or throws in strict mode)
user.email = "a@b.com"; // Can't add properties
delete user.age; // Can't delete properties
console.log(user); // { name: "Alice", age: 25 } — unchanged!Object.freeze() is shallow! It only freezes the top level. Nested objects can still be modified:
const user = Object.freeze({
name: "Alice",
address: { city: "NYC" }
});
user.name = "Bob"; // Blocked
user.address.city = "LA"; // Works! Nested object not frozen
console.log(user.address.city); // "LA"For deep freezing, you need a recursive function or use structuredClone() to create a deep copy first.
Shallow Copy vs Deep Copy
When you need a truly independent copy of an object, you have two options.
Shallow Copy: One Level Deep
A shallow copy creates a new object with copies of the top-level properties. But nested objects are still shared!
const original = {
name: "Alice",
address: { city: "NYC" }
};
// Shallow copy methods:
const copy1 = { ...original }; // Spread operator
const copy2 = Object.assign({}, original); // Object.assign
// Top-level changes are independent:
copy1.name = "Bob";
console.log(original.name); // "Alice" ✅
// But nested objects are SHARED:
copy1.address.city = "LA";
console.log(original.address.city); // "LA" 😱Deep Copy: All Levels
A deep copy creates completely independent copies at every level.
const original = {
name: "Alice",
scores: [95, 87, 92],
address: { city: "NYC" }
};
// structuredClone() — the modern way (ES2022+)
const deep = structuredClone(original);
// Now everything is independent:
deep.address.city = "LA";
console.log(original.address.city); // "NYC" ✅
deep.scores.push(100);
console.log(original.scores); // [95, 87, 92] ✅Which to use:
structuredClone()— As documented by MDN, this API is available in all major browsers since 2022 and is the recommended approach for most casesJSON.parse(JSON.stringify())— Only for simple objects (loses functions, Dates, undefined)- Lodash
_.cloneDeep()— When you need maximum compatibility
How Engines Actually Store Values
Why this section exists: Many tutorials teach that "primitives go on the stack, objects go on the heap." This is a simplification that's often wrong. Here's what actually happens.
The ECMAScript Specification Doesn't Define Storage
The ECMAScript specification defines behavior, not implementation. It never mentions "stack" or "heap." Different JavaScript engines can store values however they want, as long as the behavior matches the spec.
How V8 Actually Works
V8 (Chrome, Node.js, Deno) uses a technique called pointer tagging to efficiently represent values. According to the V8 team's blog, this optimization is critical for JavaScript performance — it allows the engine to distinguish small integers from heap pointers without additional memory lookups.
Smis (Small Integers): The Only "Direct" Values
The ONLY values V8 stores "directly" (not on the heap) are Smis — Small Integers in the range approximately -2³¹ to 2³¹-1 (about -2 billion to 2 billion).
┌─────────────────────────────────────────────────────────────────────────┐
│ V8 POINTER TAGGING │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ Smi (Small Integer): │
│ ┌────────────────────────────────────────────────────────────┬─────┐ │
│ │ Integer Value (31 bits) │ 0 │ │
│ └────────────────────────────────────────────────────────────┴─────┘ │
│ Tag bit │
│ │
│ Heap Pointer (everything else): │
│ ┌────────────────────────────────────────────────────────────┬─────┐ │
│ │ Memory Address │ 1 │ │
│ └────────────────────────────────────────────────────────────┴─────┘ │
│ Tag bit │
│ │
└─────────────────────────────────────────────────────────────────────────┘Everything Else Lives on the Heap
This includes values you might think are "simple":
| Value Type | Where It's Stored | Why |
|---|---|---|
| Small integers (-2³¹ to 2³¹-1) | Directly (as Smi) | Fixed size, fits in pointer |
| Large numbers | Heap (HeapNumber) | Needs 64-bit float |
| Strings | Heap | Dynamically sized |
| BigInts | Heap | Arbitrary precision |
| Objects, Arrays | Heap | Complex structures |
The big misconception: Strings are NOT fixed-size values stored on the stack. A string like "hello" and a string with a million characters are both stored on the heap. The variable just holds a pointer to that heap location.
String Interning
V8 optimizes identical strings by potentially sharing memory (string interning). Two variables with the value "hello" might point to the same memory location internally. But this is an optimization — strings still behave as independent values because they're immutable.
Why the Stack/Heap Model is Taught
The simplified stack/heap model is useful for understanding behavioral differences:
- Things that "behave like stack values" = act independently
- Things that "behave like heap values" = can be shared
Just know it's a mental model for behavior, not how JavaScript actually works internally.
Want to go deeper? Check out our JavaScript Engines guide for more on V8 internals, JIT compilation, and optimization.
Common Bugs and Pitfalls
Best Practices
Guidelines for working with objects:
-
Treat objects as immutable when possible
// Instead of mutating: user.name = "Bob"; // Create a new object: const updatedUser = { ...user, name: "Bob" }; -
Use
constby default — prevents accidental reassignment -
Know which methods mutate
- Mutating:
push,pop,sort,reverse,splice - Non-mutating:
map,filter,slice,concat,toSorted
- Mutating:
-
Use
structuredClone()for deep copiesconst clone = structuredClone(original); -
Clone function parameters if you need to modify them
function processData(data) { const copy = structuredClone(data); // Now safe to modify copy } -
Be explicit about intent — comment when mutating on purpose
Key Takeaways
The key things to remember:
-
Primitives vs Objects — the ECMAScript terms (not "value types" vs "reference types")
-
The real difference is mutability — primitives are immutable, objects are mutable
-
Call by sharing — JavaScript passes ALL values as copies of references; mutation works, reassignment doesn't
-
Object identity — objects are compared by identity, not content (
{} === {}is false) -
constprevents reassignment, not mutation — useObject.freeze()for true immutability -
Shallow copy shares nested objects — use
structuredClone()for deep copies -
Know your array methods —
push/pop/sortmutate;map/filter/slicedon't -
The stack/heap model is a simplification — useful for understanding behavior, not technically accurate
-
In V8, only Smis are stored directly — strings, BigInts, and objects all live on the heap
-
Symbols have identity — two
Symbol("id")are different, unlike other primitives
Test Your Knowledge
Frequently Asked Questions
Related Concepts
Primitive Types
Deep dive into the 7 primitive types and their characteristics
JavaScript Engines
How V8 compiles and optimizes your code, including memory management
Type Coercion
How JavaScript converts between types automatically
Scope and Closures
How closures capture references to variables
Reference
ECMAScript Data Types — ECMA-262
The official specification defining primitive values and objects in JavaScript.
JavaScript Data Structures — MDN
MDN's comprehensive guide to JavaScript's type system and data structures.
Object.freeze() — MDN
Documentation on freezing objects for immutability.
structuredClone() — MDN
The modern way to create deep copies of objects.
Articles
Evaluation Strategy in ECMAScript — Dmitry Soshnikov
The definitive explanation of call-by-sharing in ECMAScript by a language theory expert. Includes comparison with true pass-by-reference and detailed examples.
Is JavaScript Pass by Reference? — Aleksandr Hovhannisyan
Excellent deep-dive debunking the "pass by reference" myth. Explains true references vs object references with C++ comparisons.
Mutability vs Immutability — freeCodeCamp
Beginner-friendly guide focusing on the practical differences between mutable and immutable data in JavaScript.
JavaScript Primitive vs. Reference Values
Clear explanation with visual diagrams showing how primitives and objects behave differently.
Videos
Pass by Value vs Pass by Reference | Call by Sharing — The Code Dose
Modern explanation using the correct "call by sharing" terminology. Part of an excellent Understanding JavaScript series.
JavaScript Pass by Value vs Pass by Reference — techsith
Popular tutorial (37K+ views) with clear examples of how primitives and objects behave differently in functions.
Understanding Passing by Reference or Value — Steve Griffith
Comprehensive walkthrough covering primitives vs objects, function parameters, and common misconceptions.
