You trace a bug through three files only to find that a harmless helper function renamed a user. You never touched the original. Or so you thought. In JavaScript, assigning one variable to another does not always mean what you expect. The language splits its data into two storage strategies, and forgetting which one you are using is how silent mutations creep into production code.
To prevent this, you need to see the difference between primitive values and objects exactly as the engine does.
Primitives: Actual Copies
A primitive is a single, indivisible datum. It cannot be decomposed into smaller JavaScript values. The language defines seven primitive types: String, Number, Boolean, Undefined, Null, Symbol, and BigInt.
Because primitives are atomic, they generally live directly inside the variable binding. When you copy one primitive variable to another, the engine duplicates the actual data. Each variable receives its own independent slot in memory.
let a = "Alina";
let b = a;
b = "Ali";
console.log(a); // "Alina"
console.log(b); // "Ali"
Here, a remains untouched. Reassigning b created a brand new value and pointed b at it, while a still holds its original string. This behavior is copy by value. It works identically for numbers, booleans, symbols, and the rest of the primitive family. You can pass them into functions, reassign them, or return them without worrying about side effects on sibling variables.
Objects: Shared Addresses
Objects are different. An object is a composite container that groups multiple pieces of data together. This category includes plain objects, arrays, functions, dates, and every other non-primitive type. Because these structures can grow large and nested, JavaScript does not store the whole object inside the variable. Instead, the variable holds a reference, essentially a memory address pointing to the real data stored elsewhere.
When you assign an object to a new variable, the engine copies the address, not the object. Two variables now point to the exact same house.
const user = { name: "Alina" };
const copy = user;
copy.name = "Ali";
console.log(user.name); // "Ali"
Changing copy.name also changed user.name because both names resolve to the same underlying object. This is copy by reference. The same surprise appears with arrays:
const scores = [82, 91, 74];
const backup = scores;
backup.push(88);
console.log(scores); // [82, 91, 74, 88]
There is only one array in memory. scores and backup are merely two signs pointing at it.
The const keyword adds a layer of confusion. Declaring an object with const locks the variable binding so you cannot point it at a new address. It does not lock the object itself.
const settings = { theme: "dark" };
settings.theme = "light"; // Works perfectly.
settings = { theme: "dark" }; // TypeError
Developers often expect const to promise immutability. It does not. It only prevents reassignment of the reference. If you want the contents sealed, you must copy with intent.
Where the Real Bugs Live
Reference bugs rarely show up as obvious variable reassignments. They hide inside function calls.
function addTimestamp(record) {
record.timestamp = Date.now();
return record;
}
const original = { id: 1 };
addTimestamp(original);
console.log(original.timestamp); // A number now exists here. Oops.
The parameter record received a copy of the reference. Any property mutation inside the function wrote straight into the caller’s object. The function looked like a simple transformation, yet it leaked state across scope boundaries.
This pattern is especially painful in UI frameworks like React, where state updates depend