Introduction
In JavaScript, we often hear about primitive and non-primitive data types. We know that all data types are categorized into primitive and non-primitive groups 👍; this is our total understanding, but we don’t investigate why some data types fall under the primitive category and others are classified as non-primitive 😥.
Yes, there’s a clear distinction between the list of data types under the primitive category and those under the non-primitive category. Many developers don’t have a clear understanding of either; they simply know the names of all the data types. If we ask them about primitive and non-primitive data types, they’ll likely just list them out. However, if we ask why they are divided into these two specific categories, they won’t be able to explain it.
Today, in this article, I will explain everything about these concepts that most developers often don’t explain. I will place less emphasis on listing data types and focus more on the underlying concepts. We’ll also explore related concepts that deepen our understanding.
Let’s get started:
Primitive data types are passed by value, while non-primitive data types are passed by reference — that’s why they’re also known as reference types. In JavaScript, all primitive data types are: String, Number, BigInt, Boolean, undefined, null, and Symbol, while non-primitive data types are: Object, Array, Function, Date, RegExp, Map, Set, WeakMap, and WeakSet. Knowing this can be confusing at first, but we’ll understand it in depth. Let’s start with primitive data types.
Primitive Data
I know the phrase "Primitive data types are passed by value" can be confusing, but once we understand it, everything else becomes clearer. Check out the example below:
let a = 10;
let b = a; // b gets a copy of 10
b = 20;
console.log(a); // 10 – original remains unchanged
console.log(b); // 20
Did you see what happened? We declared a variable a and assigned it the value 10. Then, we declared another variable b and assigned it the value of a. After that, we updated the value of b to 20. When we logged a and b, we saw that a still had the value 10, while b had the updated value 20.
This might seem confusing at first — why didn't changing b also change a, since we did b = a earlier? Shouldn't a also be 20?
The answer lies in the concept: primitive data types are passed by value. This means that when we assigned b = a, we copied the value of a (which is 10) into b. We did not assign the actual variable a to b, just its value.
Since a is a primitive type (a number, in this case), it is stored by value. So, b becomes a completely separate copy with its memory location. Changing b does not affect a, because they are no longer linked after the initial assignment.
All data types that fall under the primitive category behave the same way. You can verify this if you have any doubts.
Non-Primitive Data
Non-primitive data types are also known as "reference data types." As I mentioned earlier, I'm revisiting the idea that non-primitive data types are passed by reference, just as I did when discussing primitive data types. See the example below:
let obj1 = { name: "Alice" };
let obj2 = obj1; // obj2 gets a reference to obj1
obj2.name = "Bob";
console.log(obj1.name); // Bob – original object is affected
console.log(obj2.name); // Bob
Did you notice what happened? It behaves differently from primitive data types.
We created an object obj1 with a property name set to "Alice", and then we created another variable obj2 and assigned obj1 to it. When we updated the name property using obj2, we printed the value using console.log. As you can see, both obj1 and obj2 reflect the updated value "Bob", even though we only modified obj2.
You might be wondering why this happens. It’s quite interesting once you understand it.
In JavaScript, when we assign a non-primitive data type (like an object or array) to another variable, we’re not assigning a copy of the value. Instead, we assign a reference to the same object in memory.
In our example, obj2 = obj1 means obj2 now holds a reference to the same object that obj1 refers to. That’s why a change via obj2 also affects obj1.
Now you can understand why such data types are called reference types: we’re assigning references to the actual object, not duplicates of the object.
Now that we've learned a lot about both primitive and non-primitive data types, we're already ahead of many developers 😆, because we can clearly explain something many developers often struggle with 😑.
Although this part ends here, it raises some strange questions and lingering doubts. We need to address them, which is why we’ll now explore some tricky issues and try to better understand or resolve them. Let’s quickly clear up your confusion:
Tricky Questions & Confusions
Mutating an Object Instead of Copying It
As we saw above, when we assigned obj1 to obj2 and then updated a property on obj2, the change affected both objects. This might raise a question: Is there a way to assign an object without affecting the original one?
The answer is yes! In JavaScript, we can copy an object (or any non-primitive data type) to another variable in a way that avoids sharing the same reference. One common method is using a shallow copy.
JavaScript provides several ways to create shallow copies, such as using Object.assign(), the spread operator ({ ...obj }), or utility libraries. However, all of these methods have limitations and are suitable for different scenarios.
let user1 = { name: "Alice" };
let user2 = { ...user1 };
user2.name = "Bob";
console.log(user1.name); // Output: "Alice"
console.log(user2.name) // Output: "Bob"
As you may have noticed, I used the shallow copy technique, achieved using the spread operator. In JavaScript, the spread operator is written as three dots (...) and is used to expand elements of an iterable, such as arrays, strings, or objects, into individual elements. It behaves similarly to a loop in some contexts.
If I try to cover all the techniques for copying arrays or objects, the article would become quite lengthy. This topic deserves its own dedicated blog post, which I plan to write soon. For now, let’s focus on the bugs related to the topic we’re currently discussing.
Comparing Objects or Arrays Using == or ===
This is another important concept. You might expect a result of true when comparing arrays or objects that look the same, but they are actually different in memory. In JavaScript, both == and === compare object references, not their content. That’s why two arrays with the same values are not considered equal.
const a = [1, 2, 3];
const b = [1, 2, 3];
console.log(a === b); // Output: false
console.log(a == b); // Output: false
Why does it return false even with the loose equality operator (==), as well as the strict equality operator (===)?
That’s a great question! The answer lies in how JavaScript handles reference types. Both == and === compare object references, not their contents.
The strict equality operator (===) returns true only if both the value and the data type are the same. The loose equality operator (==) allows type coercion and returns true if the values are considered equal after conversion.
However, in the case of arrays (or any objects), both == and === still check the data type and value, but what they compare is the reference in memory. So even if two arrays contain the same elements and types, they’re stored in different memory locations. That’s why the comparison returns false — because the references are not the same.
But that doesn’t mean JavaScript has no way to handle this. Just like we handled the object copying issue, JavaScript also provides a workaround.
const a = [1, 2, 3];
const b = [1, 2, 3];
console.log(JSON.stringify(a) === JSON.stringify(b)); // ✅ true
Did you notice that the output is now true? That’s because JSON.stringify converts an array into a string, a primitive data type. Once both arrays are converted to strings, we are no longer comparing two objects but two string values. Since both strings have the same content, the comparison returns true, and there's no memory reference issue involved, you can check the way it works step by step:
JSON.stringify(a) converts the array [1, 2, 3] into the string "[1,2,3]".
Similarly, JSON.stringify(b) does the same.
Now you're comparing two strings, which are primitive values, not objects. Since the strings are identical, the comparison returns true.