Back to blog
JavaScriptApril 24, 20238 min read

JavaScript Primitives vs Objects: How Stack and Heap Memory Work

A simple guide to how JavaScript handles primitive and non-primitive values, why copying a string is different from copying an object, and how stack and heap memory explain the behavior you see in real code.

JavaScript Primitives vs Objects: How Stack and Heap Memory Work

If you have ever changed one variable and then wondered why another variable changed too, you have already touched one of the most important ideas in JavaScript.

Some values behave like simple copies. Others behave like shared references.

That difference is what makes primitives and objects feel so different in day-to-day code, and the easiest way to understand it is to look at stack and heap memory.

I will keep this practical. No computer science lecture. Just the mental model you need to stop guessing.

The short version

In JavaScript, values usually fall into two groups:

  • Primitive values: string, number, boolean, null, undefined, bigint, symbol
  • Non-primitive values: object, array, function

The simple idea is this:

  • Primitive values are handled like direct values.
  • Objects are handled through references.

That is why this works the way you expect:

let firstName = "Charaf";
let copiedName = firstName;
 
copiedName = "Omar";
 
console.log(firstName); // "Charaf"
console.log(copiedName); // "Omar"

And this surprises many people at first:

const user = { name: "Charaf" };
const anotherUser = user;
 
anotherUser.name = "Omar";
 
console.log(user.name); // "Omar"
console.log(anotherUser.name); // "Omar"

Why? Because strings are copied by value, while objects are shared by reference.

A simple way to picture stack and heap

Think of memory like this:

  • The stack is a small, fast area that stores simple values and references.
  • The heap is a larger area where objects live.

This is a simplification, but it is the useful kind of simplification.

When you create a primitive value, JavaScript can treat it like a direct value.

When you create an object, the actual object data lives somewhere in the heap, and your variable holds a reference to it.

So with this code:

const product = {
  title: "Keyboard",
  price: 70,
};

You can picture it like this:

Stack:
product -> reference to heap object
 
Heap:
{ title: "Keyboard", price: 70 }

The variable does not directly contain the full object. It points to it.

Primitives: copied as their own value

Primitive values are easy to reason about. If you assign one variable to another, JavaScript copies the value.

let score = 100;
let backupScore = score;
 
backupScore = 250;
 
console.log(score); // 100
console.log(backupScore); // 250

The second variable gets its own separate value.

This is why working with numbers, booleans, and strings usually feels safe and predictable.

Real-world example

Imagine you store a discount percentage:

let discount = 15;
let displayedDiscount = discount;
 
displayedDiscount = 20;

Changing displayedDiscount does not change discount. They are separate values.

That is exactly what you want for small standalone pieces of data.

Objects and arrays: copied as references

Objects work differently.

const settings = {
  theme: "dark",
  language: "en",
};
 
const currentSettings = settings;
 
currentSettings.theme = "light";
 
console.log(settings.theme); // "light"

This does not create a fresh copy of the object.

It copies the reference.

So both variables point to the same object in memory.

The same thing happens with arrays:

const tags = ["javascript", "memory"];
const copiedTags = tags;
 
copiedTags.push("stack");
 
console.log(tags); // ["javascript", "memory", "stack"]

Again, both variables are connected to the same array.

Why this matters in real apps

This is not just theory. You feel it in real projects all the time.

Scenario 1: editing a form object

Let’s say you fetch user data and want to edit it in a form.

const originalProfile = {
  name: "Charaf",
  city: "Rabat",
};
 
const formState = originalProfile;
 
formState.city = "Casablanca";

Now originalProfile.city is also "Casablanca".

That can be a bug if you expected the form data to be independent until the user clicks save.

A safer version is:

const originalProfile = {
  name: "Charaf",
  city: "Rabat",
};
 
const formState = { ...originalProfile };
 
formState.city = "Casablanca";
 
console.log(originalProfile.city); // "Rabat"
console.log(formState.city); // "Casablanca"

Now you made a new object.

Scenario 2: shopping cart bugs

Imagine you pass a cart object into a helper function.

function addFreeSticker(cart) {
  cart.items.push("free sticker");
}
 
const cart = { items: ["mouse", "keyboard"] };
 
addFreeSticker(cart);
 
console.log(cart.items);
// ["mouse", "keyboard", "free sticker"]

This is not wrong, but it is important to know that the function changed the original object.

If you want to avoid that side effect, return a new object instead:

function addFreeSticker(cart) {
  return {
    ...cart,
    items: [...cart.items, "free sticker"],
  };
}

This pattern is common in React state updates and anywhere you want predictable data flow.

Stack vs heap with an easy analogy

Here is a simple analogy that usually helps.

Primitive value

Writing a primitive is like writing a phone number on two separate sticky notes.

let numberA = 42;
let numberB = numberA;

Now each sticky note has its own 42.

If you change one note, the other one stays the same.

Object value

Writing an object variable is more like writing the address of a house on two sticky notes.

const houseA = { color: "white" };
const houseB = houseA;

Both notes point to the same house.

If you repaint the house through one note, the house is still repainted when you visit through the other note.

That is what a shared reference feels like.

A common misunderstanding

People often say:

"Objects are stored in the heap and primitives are stored in the stack."

That is okay as a beginner shortcut, but do not hold onto it too hard.

JavaScript engines are smarter than that, and real memory management is more detailed.

The better practical rule is:

  • primitives behave like direct copied values
  • objects behave like shared references unless you make a copy yourself

That rule will help you more in real code than trying to memorize engine internals too early.

How to copy values safely

For primitives

You usually do not need to do anything special.

let a = "hello";
let b = a;

That is already a safe value copy.

For objects

Use a shallow copy when you only need a new top-level object.

const user = { name: "Charaf", age: 24 };
const copiedUser = { ...user };

For arrays:

const numbers = [1, 2, 3];
const copiedNumbers = [...numbers];

But remember: this is only a shallow copy.

If the object contains nested objects, the inner references are still shared.

const account = {
  name: "Charaf",
  address: {
    city: "Rabat",
  },
};
 
const clonedAccount = { ...account };
 
clonedAccount.address.city = "Tangier";
 
console.log(account.address.city); // "Tangier"

That happens because address is still the same nested object reference.

How this helps you debug faster

When you see weird data changes in JavaScript, ask yourself one question first:

Am I dealing with a copied value or a shared reference?

That one question can explain a lot of bugs, especially in:

  • form handling
  • React state
  • API response transformations
  • helper functions that mutate data
  • array and object updates in loops

Final takeaway

If you remember only one thing, remember this:

  • Primitives are copied as values.
  • Objects and arrays are copied as references.

And the stack vs heap model gives you a simple way to picture why that happens.

Once this clicks, JavaScript starts feeling much less random.

The next time an object changes "by itself," you will know it probably did not. Two variables were just pointing to the same place.