JavaScript Basics: Primitives, Object References, and Mutation
March 19, 2026
In today’s article, I want to focus on three foundational JavaScript ideas:
- How JavaScript stores and works with values
- Why primitives behave differently from objects
- Why mutation causes so many bugs in real applications, including Angular apps
These ideas sound basic at first, but they are extremely important. They affect how we reason about state, debugging, equality, copying data, and predictable UI updates.
JavaScript Values
When thinking about JavaScript values, it helps to separate them into two categories:
- primitive values
- reference values like objects and arrays
JavaScript stores primitive values as the actual value itself. Objects, arrays, and functions are different: variables hold a reference to the underlying object in memory.
JavaScript Primitives
These are primitive values in JavaScript:
- string
- number
- boolean
- null
- undefined
- symbol
- bigint
These are not primitives:
- objects
- arrays
- functions
- dates
- maps/sets
This distinction matters because it affects how assignment works.
When assigning a primitive, JavaScript copies the value.
When assigning an object, JavaScript copies the reference value, not a brand new object.
Primitive Example
When assigning one primitive variable to another, the value is copied.
let a = 5;
let b = a;
b = 10;
In this example:
astarts with the value5bgets its own copy of that value- when
bis reassigned to10,astays5
That is because primitives are independent values after assignment.
Object Reference Example
Objects behave differently.
const person1 = { name: "Alex" };
const person2 = person1;
person2.name = "Jordan";
This is where things get interesting.
After this runs, person1.name is also "Jordan".
Why?
Because person1 does not contain the object directly in the same way a primitive contains its value. Instead, it holds a reference to that object. When we do:
const person2 = person1;
person2 now holds the same reference. Both variables point to the same underlying object.
So when we mutate the object through person2, we are still changing the same object that person1 points to.
Why Mutation Causes Bugs
A mutation bug happens when shared data is changed unexpectedly.
For example:
- one part of the app changes an object
- another part of the app is also using that same object
- now both places see the changed data, even if that was not intended
This becomes a big deal in frontend apps because shared state can lead to:
- hard-to-track side effects
- confusing bugs
- unexpected UI behavior
- brittle code that is harder to reason about
This is especially important in Angular when working with:
- component state
- inputs
- signals
- RxJS streams
- change detection
- immutable update patterns
Coding Examples
Save this into a .js file and inspect the console output:
console.log("--- PRIMITIVES ---");
const str = "hello";
const num = 42;
const bool = true;
const nothing = null;
let notAssigned;
const unique = Symbol("id");
const big = 9007199254740993n;
console.log(typeof str, str);
console.log(typeof num, num);
console.log(typeof bool, bool);
console.log(typeof nothing, nothing); // weird historical JS behavior
console.log(typeof notAssigned, notAssigned);
console.log(typeof unique, unique);
console.log(typeof big, big);
console.log("\n--- PRIMITIVE COPY ---");
let a = 10;
let b = a;
console.log("before change:", { a, b });
b = 99;
console.log("after change:", { a, b });
console.log("\n--- OBJECT REFERENCE ---");
const person1 = { name: "Alice", age: 30 };
const person2 = person1;
console.log("before mutation:", { person1, person2 });
person2.age = 31;
console.log("after mutation:", { person1, person2 });
console.log("\n--- ARRAY REFERENCE ---");
const arr1 = [1, 2, 3];
const arr2 = arr1;
console.log("before push:", { arr1, arr2 });
arr2.push(4);
console.log("after push:", { arr1, arr2 });
console.log("\n--- SHALLOW COPY OBJECT ---");
const originalUser = { name: "Sam", address: { city: "Chicago" } };
const copiedUser = { ...originalUser };
console.log("before nested mutation:", { originalUser, copiedUser });
copiedUser.name = "Jordan";
copiedUser.address.city = "Boston";
console.log("after nested mutation:", { originalUser, copiedUser });
console.log("\n--- COMPARISONS ---");
console.log(5 === 5); // true
console.log("hi" === "hi"); // true
console.log({} === {}); // false
console.log([] === []); // false
const ref1 = { x: 1 };
const ref2 = ref1;
console.log(ref1 === ref2); // true
Highlights to Pay Attention To
1. typeof null is "object"
This is one of JavaScript’s famous historical quirks. null is not an object in the way we normally think about objects, but typeof null still returns "object" for legacy reasons.
2. Primitive assignment copies the value
Changing b does not affect a.
3. Object assignment copies the reference value
Changing person2.age also changes person1.age because both variables refer to the same object.
4. Spread syntax is only a shallow copy
This part is very important:
const copiedUser = { ...originalUser };
This creates a new top-level object, but nested objects are still shared.
So this line:
copiedUser.name = "Jordan";
does not affect originalUser.name.
But this line:
copiedUser.address.city = "Boston";
does affect originalUser.address.city.
That is because the nested address object is still shared between both objects.
Safer Nested Copy Example
If you want to avoid that nested mutation bug, you need to copy the nested object too:
const copiedUser = {
...originalUser,
address: {
...originalUser.address,
city: "Boston",
},
};
This pattern is much safer because it creates a new address object instead of reusing the old one.
Common Beginner Mistakes to Avoid
1. Saying objects are “passed by reference”
This is common, but not the most precise phrasing.
A better way to say it is:
JavaScript passes values. For objects, that value happens to be a reference.
That is more accurate.
2. Thinking spread syntax does a deep clone
It does not. It only copies the first level.
3. Assuming equality means “same shape”
For example:
{ a: 1 } === { a: 1 } // false
Even though those objects look the same, they are different object references, so they are not strictly equal.
Why This Matters in Angular
This topic matters a lot in Angular development.
If you do not understand value vs reference well, it becomes much harder to reason about:
- why shared state changes unexpectedly
- why a component re-renders or does not re-render
- how to safely update state
- how
OnPushchange detection works - why immutable update patterns are helpful
This is one of those JavaScript fundamentals that directly improves frontend architecture and debugging skills.
Final Takeaway
The biggest lesson for me from this refresher is this:
- primitives behave like independent values after assignment
- objects and arrays can easily become shared state through references
- mutation bugs often happen when we forget that two variables may still point to the same underlying object
Understanding this makes JavaScript feel much less mysterious and makes it easier to write safer frontend code.
