Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 24

Memory & Mutability: Primitive vs. Reference Types

Recommended resources

Hello! Welcome to the first lesson in our module on JavaScript's core concepts.

In our previous module, we wrapped up advanced CSS by exploring the dynamic power of custom properties. Now, we transition from the world of styling and presentation to the engine room of the modern web: JavaScript. Your background in software development and computer science will be particularly valuable here, as we'll be exploring the language's architecture from the ground up.

Today, we'll start with a concept that is fundamental to understanding almost everything else in JavaScript: the distinction between its data types. By the end of this lesson, you will be able to explain how primitive and reference types differ in memory storage and mutation behavior.

We'll begin with the conventional mental model and then, to satisfy your preference for "under-the-hood" mechanics, we'll look at how JavaScript engines like V8 actually handle these values in memory.

The Two Categories of Types

In JavaScript, all data values belong to one of two categories: primitive types or reference types.

Primitive Types: There are seven primitive types. They are considered "primitive" because they are the simplest, most fundamental data units in the language.

  • string
  • number
  • bigint
  • boolean
  • undefined
  • null
  • symbol

Reference Types: Everything else is a reference type, but they are all technically of the object type. This includes:

  • Objects ({})
  • Arrays ([])
  • Functions (function() {})
  • Dates (new Date())
  • and all other built-in objects.

The core difference between these two categories lies in how they are stored in memory and how they behave when you assign them from one variable to another. A common and useful way to visualize this is with the concepts of the stack and the heap.

JavaScript Under The Hood [4] - Memory Storage

To start, let's watch a clear explanation of this foundational model. This video from Traversy Media introduces the stack and heap and uses diagrams to show how primitives and reference types are handled differently.

Please watch from 2:32 to 9:39. Focus on these key points: Static vs. Dynamic Data (2:32 - 4:08): Understand why primitives are considered 'static' (fixed-size) and stored on the stack, while objects are dynamic and stored on the heap. Diagrams (4:08 - 6:52): This is the most important part. Pay close attention to how the diagrams illustrate the assignment of primitives versus objects. Notice that when an object is assigned to a new variable, both variables point to the same location on the heap. Code Demo (6:52 - 9:39): The video reinforces the diagram's concepts with a live code example, demonstrating how mutating an object through one variable affects the other.

As the video demonstrates, when you assign a primitive value to another variable, you are creating a copy of the value. When you assign a reference type, you are creating a copy of the reference (the memory address), not the object itself.

This image provides a clear visualization of this reference mechanism:

Memory Storage for Primitive and Reference Types in JavaScript
This diagram illustrates the conceptual memory model. Primitive types like `'Vinoo'` are stored directly in the variable's location on the call stack. Reference types, like the `emp` object, store a memory address (a pointer) on the stack, which in turn points to the actual object data located on the heap. When `newEmp` is assigned `emp`, it's this memory address that gets copied, not the object itself.

The Real Distinction: Mutability

The concepts of stack and heap help explain the behavior we see, but the fundamental property that defines that behavior is mutability.

  • Primitives are immutable: You cannot change a primitive value. When you perform an operation that seems to modify it (like name = name + '!'), you are actually creating a new primitive value and reassigning the variable to point to it. The original value is unchanged.
  • Objects are mutable: You can change the internal state of an object. When you modify a property (like person.age = 41), you are directly mutating the object in its memory location on the heap.

This is why changing an object through newPerson in the video also changed what person pointed to—they both held a reference to the same mutable object.

Primitives vs Objects: How JavaScript Values Actually Work

The article 'Primitives vs Objects' from 33 JavaScript Concepts provides a technically precise explanation that goes beyond the simplified model. It correctly frames the entire discussion around mutability.

Please read the following sections to solidify this idea: 'The Real Distinction: Mutability': This introduces the core concept. 'How Primitives and Objects Behave': Review the tables and examples that show how primitives are immutable and independent, while objects are mutable and shared. 'Copying Behavior: The Critical Difference': This section contains the 'Array Gotcha', a classic example of bugs that arise from misunderstanding object references.


Passing Arguments to Functions: "Call by Sharing"

A frequent point of confusion is how these types behave when passed as arguments to functions. You may have heard the terms "pass by value" and "pass by reference." In JavaScript, the evaluation strategy is more accurately described as call by sharing.

Here’s what it means:

  1. When you pass a variable to a function, JavaScript creates a copy of the variable's value.
  2. If the variable holds a primitive, the function gets a copy of that primitive value.
  3. If the variable holds a reference type, the function gets a copy of the reference (the memory address).

The crucial rule is:

  • Mutating the object inside the function will affect the original object outside the function.
  • Reassigning the parameter to a completely new value inside the function will not affect the original variable outside the function.

Primitives vs Objects: How JavaScript Values Actually Work

Let's return to the 'Primitives vs Objects' article, which has one of the best explanations of this topic.

Read the section titled 'Call by Sharing: How JavaScript Passes Arguments'. Pay close attention to the diagrams distinguishing between mutation and reassignment. This section correctly explains why JavaScript is not truly 'pass by reference' and how the same rules apply to both primitives and objects—you just can't observe mutation with primitives because they are immutable.


How JavaScript Engines Actually Store Values

So far, we've used the "stack for primitives, heap for objects" model. This is a powerful and useful mental model for predicting behavior. However, since you're interested in the underlying mechanics, it's time to look at the technical reality inside an engine like V8 (used by Chrome and Node.js).

The ECMAScript specification defines behavior, not implementation. It never mentions the words "stack" or "heap". The simplified model is often wrong on one key point: strings. A string can be of any length, so it's not a fixed-size value that can be easily placed on the stack.

In V8:

  • The only values stored "directly" without pointing to the heap are small integers (called "Smis"). V8 uses a trick called pointer tagging where the last bit of a memory address indicates if the value is a Smi or a pointer to the heap.
  • Everything else lives on the heap. This includes large numbers, strings, BigInts, and of course, all objects.

So, a variable holding the number 100 might be a Smi, but a variable holding the string "hello" actually holds a pointer to that string's location on the heap, just like an object does.

Why doesn't this break our model? Because strings are immutable. Even though they live on the heap, you can't mutate them. So, they behave as if they are copied by value, preserving the correctness of our mental model.

Primitives vs Objects: How JavaScript Values Actually Work

The 'Primitives vs Objects' article concludes by debunking this common myth. This section provides the technical accuracy you're looking for.

Read the section 'How Engines Actually Store Values'. It explains pointer tagging, Smis, and clarifies that strings are, in fact, stored on the heap. This reconciles the simplified model with the technical implementation.


Conclusion

Understanding the distinction between primitives and reference types is crucial for writing predictable and bug-free JavaScript. While the simplified "stack vs. heap" model is excellent for reasoning about behavior, the true underlying difference is mutability.

Key Takeaways:

  • Primitives (string, number, etc.) are immutable. When assigned or passed, their value is copied.
  • Reference Types (object, array, etc.) are mutable. When assigned or passed, their reference (pointer) is copied, leading to multiple variables pointing to the same object.
  • Mutating an object through any of its references affects the single, shared object.
  • JavaScript's argument-passing strategy is call by sharing, where a copy of the value (which can be a primitive or a reference) is passed to the function.
  • While the stack/heap model is a useful simplification, in reality, engines like V8 store most data types, including strings, on the heap, with optimizations for small integers (Smis).

What's Next?

Now that you have a solid grasp of JavaScript's data types and their memory behavior, we can explore how the language handles them in operations. In the next lesson, we will investigate a topic that often trips up developers: type coercion. We will cover how it works in equality comparisons (== vs. ===) and arithmetic operations, building directly on the foundation we've established today.