Object references and copying
One rule separates objects from primitives more than any other: primitives (strings, numbers, booleans, and the rest) are copied as a whole value, while objects are stored and passed around by reference. Get this one idea right and a whole class of “why did my other variable change?” bugs stops surprising you.
The fastest way to internalize it is to watch what the engine actually does when you copy something.
Primitives copy as a value
Start with a string, since strings are primitives:
let message = "Welcome!";
let phrase = message;
The assignment takes the value inside message and drops an independent copy into phrase. You now have two variables, and each one holds its own "Welcome!". Change one and the other doesn’t flinch.
That result probably matches your intuition. Objects break it.
A variable holds a reference, not the object
Here’s the key sentence:
A variable assigned to an object doesn’t store the object itself. It stores the object’s address in memory — a reference to it.
Consider this object:
let member = {
name: "Maya"
};
The object data lives somewhere in memory. The member variable holds a small value that points at that location.
Picture the member variable as a slip of paper with an address written on it. The object is the house at that address. When you read member.name, the engine follows the address, walks into the house, and reads the name property off the real object. The variable never contains the house — only directions to it.
This indirection is what makes the next part matter.
Copying an object copies the reference
When you copy an object variable, you copy the reference. The object itself is not duplicated.
let member = { name: "Maya" };
let editor = member; // copies the reference
Now member and editor are two separate variables, but both slips of paper carry the same address. There’s still exactly one object.
Because both variables point at the same object, either one can read or change it:
let member = { name: 'Maya' };
let editor = member;
editor.name = 'Raj'; // changed through the "editor" reference
alert(member.name); // 'Raj' — the change is visible through "member" too
Think of a cabinet with two keys. editor is one key, member is the other. Open the cabinet with editor, rearrange what’s inside, then open it later with member — same cabinet, same rearranged contents. There was never a second cabinet.
See it for yourself. Below, editor and member are two variables copied from one object. Rename through editor and watch the change surface through member too — because there is only one object behind both names:
Comparison by reference
Two object references are equal only when they point at the very same object. Identical-looking contents are not enough.
let a = {};
let b = a; // copy the reference
alert( a == b ); // true — both reference the same object
alert( a === b ); // true
Here a and b share one address, so they compare as equal with both == and ===.
Now two objects created separately:
let a = {};
let b = {}; // two independent objects
alert( a == b ); // false
They look the same — both empty — but they live at different addresses, so they’re different objects. Equality checks the reference, not the shape.
Cloning and merging with Object.assign
Copying an object variable just adds another reference. So how do you get a real, independent duplicate?
One way: build a fresh object and copy the source’s properties into it one by one.
let member = {
name: "Maya",
age: 27
};
let clone = {}; // a new empty object
// copy every property from member into clone
for (let key in member) {
clone[key] = member[key];
}
// clone is now independent with the same contents
clone.name = "Raj"; // change it
alert( member.name ); // still Maya — the original is untouched
Each clone[key] = member[key] copies a primitive value (name and age here), so the clone stands on its own.
The built-in Object.assign does the same copying for you. Its shape:
Object.assign(dest, ...sources)
destis the target object that receives properties.- The rest are source objects to copy from.
It copies every property from each source into dest, left to right, and returns dest. Use it to merge several objects into one:
let member = { name: "Maya" };
let contact = { email: "[email protected]" };
let prefs = { theme: "dark" };
// copy all properties from both sources into member
Object.assign(member, contact, prefs);
// member is now { name: "Maya", email: "[email protected]", theme: "dark" }
alert(member.name); // Maya
alert(member.email); // [email protected]
alert(member.theme); // dark
When a property already exists on the target, the incoming value wins:
let member = { name: "Maya" };
Object.assign(member, { name: "Raj" });
alert(member.name); // Raj
Point the target at a fresh empty object and Object.assign becomes a one-line clone:
let member = {
name: "Maya",
age: 27
};
let clone = Object.assign({}, member);
alert(clone.name); // Maya
alert(clone.age); // 27
All of member’s properties land in the new empty object, which is returned as clone.
Nested cloning
The clones above worked because every property was a primitive. But a property can itself hold a reference to another object:
let member = {
name: "Maya",
stats: {
height: 178,
weight: 74
}
};
alert( member.stats.height ); // 178
Now Object.assign copies the stats property — but that property is a reference. Copying it copies the address, not the inner object. So the clone and the original end up sharing the same stats:
let member = {
name: "Maya",
stats: {
height: 178,
weight: 74
}
};
let clone = Object.assign({}, member);
alert( member.stats === clone.stats ); // true — same nested object
// they share stats
member.stats.weight = 82; // change it through one variable
alert(clone.stats.weight); // 82 — seen through the other
This is what “shallow copy” means: the top level is duplicated, but nested objects are still shared by reference.
To make the two objects fully independent, the clone has to walk into every nested object and replicate its structure too, recursively. That’s a “deep clone” (also called structured cloning). You could write the recursion yourself, or use the built-in structuredClone.
structuredClone
structuredClone(object) returns a deep copy — every nested object is duplicated, all the way down.
let member = {
name: "Maya",
stats: {
height: 178,
weight: 74
}
};
let clone = structuredClone(member);
alert( member.stats === clone.stats ); // false — different objects now
// fully independent
member.stats.weight = 82; // change one
alert(clone.stats.weight); // 74 — the other is unaffected
Put both clone styles side by side. Drag the slider to change member.stats.weight on the original, and watch which copies follow along. The shallow copy shares the nested stats object, so it tracks every change; the deep copy owns its own stats and stays put:
structuredClone handles most data you’ll throw at it: plain objects, arrays, and primitive values, nested to any depth.
It even survives circular references — an object whose property points back at the object itself, directly or through a chain:
let member = {};
// create a circular reference:
// member.me points back at member
member.me = member;
let clone = structuredClone(member);
alert(clone.me === clone); // true
Notice clone.me points at clone, not at the original member. The cycle was rebuilt correctly inside the copy — a naive recursive cloner would loop forever here.
There’s a limit, though. structuredClone can’t copy function properties:
// error
structuredClone({
f: function() {}
});
Functions aren’t part of what the structured clone algorithm supports, so this throws.
For cases like that, combine cloning strategies, write your own code, or reach for a battle-tested implementation such as _.cloneDeep(obj) from the lodash library rather than reinventing it.
Summary
Objects are assigned and copied by reference. A variable never holds the object’s value directly — it holds a reference, the address of the object in memory. Copying that variable, or passing it into a function, copies the reference, not the object.
Every operation through a copied reference — adding a property, removing one, changing a value — lands on the same single shared object.
To get a genuine independent copy, clone it:
Object.assign({}, obj)or spread{...obj}for a shallow copy — top level duplicated, nested objects still shared by reference.structuredClone(obj)for a deep copy — nested objects duplicated too, with circular references handled.- A custom function or
_.cloneDeep(obj)when your data has functions or other things the built-ins won’t touch.