Primitives vs Objects: How JavaScript Values Actually Work
Learn how JavaScript primitives and objects differ in behavior. Understand immutability, call-by-sharing semantics, why mutation works but reassignment doesn’t, and how V8 actually stores values.
Have you ever wondered why changing one variable unexpectedly changes another? Why does this happen?
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 {} === {} returns false (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!
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
That’s it. The spec never mentions “value types,” “reference types,” “stack,” or “heap.” These are implementation details that vary by JavaScript engine.
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.
Copy
Ask AI
let greeting = "hello";let shout = greeting.toUpperCase();console.log(greeting); // "hello" — unchanged!console.log(shout); // "HELLO" — new string
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!
Copy
Ask AI
┌─────────────────────────────────────────────────────────────────────────┐│ 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.
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.
If you reassign the parameter to a new object, it only changes the local variable:
Copy
Ask AI
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:
Copy
Ask AI
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.”
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.
Copy
Ask AI
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 value
The same “reassignment doesn’t work” rule applies to primitives. It’s just that with primitives, there’s no mutation to try anyway!
When you copy an object variable, you copy the reference. Both variables now point to the SAME object:
Copy
Ask AI
let obj1 = { name: "Alice" };let obj2 = obj1; // obj2 gets a copy of the REFERENCEobj2.name = "Bob"; // modifies the SAME object!console.log(obj1.name); // "Bob" (changed!)console.log(obj2.name); // "Bob"
Arrays are objects too, so they behave the same way:
Copy
Ask AI
let arr1 = [1, 2, 3];let arr2 = arr1; // arr2 points to the SAME arrayarr2.push(4); // modifies the shared arrayconsole.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.
console.log({} === {}); // false — two different empty objectsconsole.log([] === []); // false — two different empty arraysconsole.log([1,2] === [1,2]); // false — two different arrays
How to compare objects/arrays by content:
Copy
Ask AI
// Simple (but limited) approachJSON.stringify(obj1) === JSON.stringify(obj2)// For arrays of primitivesarr1.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.
Common 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.
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.
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.
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).
Copy
Ask AI
┌─────────────────────────────────────────────────────────────────────────┐│ V8 POINTER TAGGING │├─────────────────────────────────────────────────────────────────────────┤│ ││ Smi (Small Integer): ││ ┌────────────────────────────────────────────────────────────┬─────┐ ││ │ Integer Value (31 bits) │ 0 │ ││ └────────────────────────────────────────────────────────────┴─────┘ ││ Tag bit ││ ││ Heap Pointer (everything else): ││ ┌────────────────────────────────────────────────────────────┬─────┐ ││ │ Memory Address │ 1 │ ││ └────────────────────────────────────────────────────────────┴─────┘ ││ Tag bit ││ │└─────────────────────────────────────────────────────────────────────────┘
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.
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.
Question 1: What's the difference between primitives and objects?
Answer:
Primitives are immutable — you cannot change a primitive value, only replace it. Copies behave independently.
Objects are mutable — you CAN change an object’s contents. Multiple variables can point to the same object.
The distinction is about mutability, not storage location.
Question 2: What does this code output?
Copy
Ask AI
let a = { count: 1 };let b = a;b.count = 5;console.log(a.count);
Answer:5Both a and b point to the same object. When you modify b.count, you’re modifying the shared object, which a also sees. This is because mutation affects the shared object.
Question 3: Why does {} === {} return false?
Answer: Because === compares identity (same object), not contents.Each {} creates a NEW empty object in memory. Even though they have the same contents (both empty), they are different objects.
Copy
Ask AI
{} === {} // false (different objects)const a = {};const b = a;a === b // true (same object)
Question 4: What's the difference between call by sharing and pass by reference?
Answer:
Call by sharing: Function receives a copy of the reference. Mutation works, but reassignment only changes the local parameter.
Pass by reference (C++ style): Parameter is an alias for the argument. Reassignment WOULD change the original.
JavaScript uses call by sharing. That’s why this doesn’t work:
Copy
Ask AI
function replace(obj) { obj = { new: "object" }; // Only changes local parameter}let x = { old: "object" };replace(x);console.log(x); // { old: "object" } — unchanged!
Question 5: Does const prevent object mutation?
Answer: No!const only prevents reassignment — you can’t make the variable point to a different value. But you CAN still mutate the object’s contents.
Question 6: Are strings really stored on the stack?
Answer: No! This is a common myth.In V8, only Smis (small integers) are stored directly. Strings are dynamically-sized and stored on the heap. The variable holds a pointer to the string’s location in heap memory.The “stack vs heap” model is a mental model for behavior, not how JavaScript actually works.
Question 7: What's the difference between shallow and deep copy?
Answer:
Shallow copy creates a new object but shares nested objects
Deep copy creates independent copies at ALL levels
What is the difference between primitives and objects in JavaScript?
Primitives (string, number, bigint, boolean, undefined, null, symbol) are immutable — you cannot change a primitive value, only replace it. Objects (including arrays, functions, and dates) are mutable — you can change their contents. According to the ECMAScript specification, this mutability distinction is the fundamental behavioral difference between the two categories.
Is JavaScript pass by value or pass by reference?
Neither. JavaScript uses “call by sharing,” a strategy first described by Barbara Liskov in 1974. All values — both primitives and objects — are passed as copies of references. This means mutation of an object parameter affects the original, but reassigning the parameter does not. This is why obj.name = "Bob" works inside a function but obj = newObj does not change the caller’s variable.
Why does changing a copied object affect the original in JavaScript?
When you write let copy = original, you copy the reference (the “key to the house”), not the object itself. Both variables point to the same object in memory. As documented in MDN, use structuredClone() for a deep copy or the spread operator ({...obj}) for a shallow copy to create independent duplicates.
How do you create a deep copy of an object in JavaScript?
Use structuredClone(original), which was standardized in 2022 and is available in all modern browsers and Node.js 17+. For older environments, JSON.parse(JSON.stringify(obj)) works for simple objects but loses functions, Dates, undefined, and circular references. Libraries like Lodash offer _.cloneDeep() for maximum compatibility.
Why does {} === {} return false in JavaScript?
Objects are compared by identity (reference), not by content. Each {} literal creates a new, distinct object in memory. Even though both are empty, they occupy different memory addresses. The ECMAScript specification defines this as the “Strict Equality Comparison” algorithm — for objects, it checks whether both operands refer to the exact same object.