Assigning one variable to another always looks like the same operation. With a number, the second variable becomes independent. With an object, both names point at the same thing, and changing one changes the other.
This asymmetry is not a JavaScript oddity: most languages work that way. You still have to know which side each type sits on.
Definition
The first level really is detached: profile.name still reads "Ada". Now switch the target to copy.address.city, because that is where the protection stops.
The table is a simplified view of memory: on the last two rows every solid frame is one object, and a frame straddling both columns is a single object reached through both names. The dashed frames on top are values, not objects, and are left out of the count. { ...profile } only copies own enumerable properties at the first level, and freezes getters into plain values at that moment. structuredClone() has limits of its own: it refuses a function, and it returns the object without its prototype, so without its class methods. The number has no copy mode at all: a primitive is duplicated on every assignment, there is no address to share.
Change the copy and watch which original moved. The third case, the nested object, is the one that surprises most.
A primitive value is copied on every assignment: the new variable gets its own instance. An object is not: the variable only holds a reference, an address, and assigning it elsewhere merely duplicates that address.
- Copied by value: Number, String, Boolean, null, undefined, Symbol and BigInt.
- Shared by reference: Object, Array, function, Date, Map and Set.
let a = 10;
let b = a;
b += 5;
console.log(a, b); // 10 15
const objectA = { total: 10 };
const objectB = objectA;
objectB.total += 5;
console.log(objectA.total, objectB.total); // 15 15In the second case, no object was created on the assignment line. There is one object and two names pointing at it.
What it changes for equality
Two objects with identical contents are never equal: the comparison looks at the address, not at the contents.
console.log({ x: 1 } === { x: 1 }); // false
console.log([1, 2] === [1, 2]); // false
const shared = { x: 1 };
console.log(shared === shared); // trueThat holds for Strict equality (===) as much as for Loose equality (==). To compare contents you either walk field by field, or serialize through JSON, keeping in mind that key order then matters.
Copying for real
const profile = { name: "Ada", address: { city: "London" } };
const shallow = { ...profile };
shallow.address.city = "Paris";
console.log(profile.address.city); // Paris, the nested part is shared
const deep = structuredClone(profile);
deep.address.city = "Lyon";
console.log(profile.address.city); // Paris, the original is protectedSpread (...) only copies one level. For a nested structure, structuredClone() produces a complete copy, dates and Maps included, where a round trip through JSON would have lost them.
Frequently asked questions
Can a function modify an object passed as an argument?
Yes, the function receives the same reference and can change the contents. Reassigning the parameter inside, however, changes nothing for the caller: only the local copy of the address is replaced. That nuance is what separates modifying an object from replacing it.
Why are two identical dates not equal?
Because a Date is an object, and therefore compared by reference. Two instances built on the same instant remain two distinct objects. The reliable comparison goes through their numeric value, with a.getTime() === b.getTime().
Are strings really copied every time?
From the language point of view, yes: a string behaves like a value and cannot mutate. Internally, engines share memory as long as nobody modifies anything, but that optimization is invisible and changes no observable behavior.