Skip to main content

Difference between primitives and non-primitives in JavaScript

Primitives are immutable values stored directly in memory by value; non-primitives (objects) are mutable structures accessed through a reference to heap memory.

Theory

TL;DR

  • A primitive is like a printed postcard: copying it gives you a fully independent duplicate, and changing yours doesn't touch the original
  • An object is like a shared Google Doc link: everyone holding that link edits the same document
  • Assignment with a primitive copies the value; assignment with an object copies the reference (a pointer)
  • Use primitives for simple, stable data (age, ID, flag); use objects for grouped or changing data (cart, config, user profile)
  • === on primitives compares values; === on objects checks whether they're the same reference in memory

Quick example

javascript
// Primitives: assignment copies the value let a = 5; let b = a; // b gets its own copy of 5 a = 10; console.log(a); // 10 console.log(b); // 5 — not affected // Objects: assignment copies the reference let x = { count: 5 }; let y = x; // y points to the same object as x x.count = 10; console.log(x.count); // 10 console.log(y.count); // 10 — same object, same result

Both x and y point to one object in memory. There is no second object. So mutating through x is visible through y.

Key difference

When you write let b = a with a primitive, JavaScript creates a new memory slot and copies the actual value into it. When you write let y = x with an object, JavaScript copies only the address of the object (a 64-bit pointer in V8). Both variables point to the same heap location. Any mutation through one variable shows up in the other.

When to use

  • Age, price, toggle flag, user ID -> primitive
  • Shopping cart, user profile, app config -> object
  • Unique key that should not accidentally equal anything else -> Symbol (primitive)
  • Grouped data with methods -> object

Comparison table

AspectPrimitivesNon-primitives (Objects)
StorageStack (direct value)Heap (data) + stack (reference)
AssignmentCopies valueCopies reference
MutabilityImmutableMutable
=== comparisonChecks valueChecks reference
SizeFixed, smallDynamic
Typesstring, number, boolean, null, undefined, bigint, symbol{}, [], () => {}
When to useConstants, IDs, calculationsData structures, configs, state

How it works internally

V8 (Chrome and Node.js) allocates primitives on the stack for fast access with no garbage collection needed. Objects go on the heap; the stack holds only a 64-bit pointer to that heap location. When you do obj.prop = 1, V8 follows the pointer and mutates the shared heap data. When no references point to an object anymore, the garbage collector frees it.

One detail worth knowing: V8 interns short identical strings, meaning they can share a single memory slot. That is a performance optimization you do not control, but it is why identical string primitives are always === equal.

Common mistakes

Mistake 1: assuming arrays copy on assignment

javascript
let arr1 = [1, 2]; let arr2 = arr1; // copies reference, not elements arr2.push(3); console.log(arr1); // [1, 2, 3] — surprise // Fix: use spread let arr2 = [...arr1];

Mistake 2: trying to mutate a string

javascript
let str = 'hello'; str[0] = 'H'; console.log(str); // 'hello' — strings are immutable, assignment has no effect // Fix: build a new string str = 'H' + str.slice(1); // 'Hello'

Mistake 3: comparing objects with ===

javascript
console.log({ a: 1 } === { a: 1 }); // false — two different references // Fix: compare by content JSON.stringify(obj1) === JSON.stringify(obj2); // For nested structures: lodash isEqual

Mistake 4: forgetting null is a primitive

javascript
let obj = null; obj.prop = 'test'; // TypeError: Cannot set properties of null // Fix: guard before accessing if (obj) obj.prop = 'test';

Real-world usage

  • React: state is an object (useState({ name: 'Alice' })); update it by creating a new object ({ ...user, name: 'Bob' }) because mutating directly bypasses React's change detection
  • Express: req.body is a mutable object; res.status(200) takes a primitive number
  • Node.js: process.env is an object shared across the app; parseInt(process.env.PORT) gives a primitive you can pass around safely
  • Lodash cloneDeep: when you need a fully independent copy of a nested object, not just a pointer copy

In practice, the object reference model causes bugs more often than the value model. Most "why did my state change unexpectedly?" questions in React trace back to shared references.

Follow-up questions

Q: What happens when you do let x = 5; x = { value: 5 };?
A: x changes from holding a primitive number to holding a reference to a new object. JavaScript does not enforce types on variables, so the reassignment just works. The number 5 is gone.

Q: Why does typeof null return 'object'?
A: It is a bug from 1995 that was never fixed to preserve backward compatibility. null is a primitive. The typeof result is wrong, but changing it would break too much existing code.

Q: Can primitives have methods like .toUpperCase() or .toString()?
A: Yes, through auto-boxing. When you call 'hello'.toUpperCase(), JavaScript temporarily wraps the string primitive in a String object, calls the method, then discards the wrapper. The original primitive stays unchanged.

Q: If you pass an array into a function and push an element inside, does the original array change?
A: Yes. Arrays are objects, so passing one passes the reference. Any mutation inside the function affects the original. To avoid this, pass a copy: fn([...arr]).

Q: (Senior) Two objects with identical properties give === false. What are the ways to check structural equality?
A: JSON.stringify(a) === JSON.stringify(b) works for flat objects with consistent key order. For nested structures or objects with Date or RegExp, use lodash isEqual, which handles edge cases correctly.

Examples

Primitive: value copy inside a function

javascript
function increment(n) { n += 1; return n; } let count = 5; let result = increment(count); console.log(count); // 5 — original unchanged console.log(result); // 6

The function receives a copy of 5. Changing n inside does nothing to count outside. This is the value model in action.

Object reference: React state mutation bug

javascript
// BAD: mutates the existing state object const [user, setUser] = useState({ name: 'Alice', prefs: { theme: 'dark' } }); user.prefs.theme = 'light'; // direct mutation setUser(user); // React sees the same reference — no re-render // GOOD: create a new object at every level that changed setUser({ ...user, prefs: { ...user.prefs, theme: 'light' } }); // React sees a new reference, triggers re-render correctly

setUser(user) passes the same pointer. React's shallow compare sees an identical reference and skips the re-render. The fix is always to return a new object, because React relies on reference inequality to detect changes.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?