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.

message"Welcome!"
phrase"Welcome!"
Two variables, two separate copies of the same string value.

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.

member
{ object }
name: “Maya”
The member variable holds a reference; the object data lives elsewhere in memory.

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.

member
editor
{ object }
name: “Maya”
One object, two variables — both references point at the same data.

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:

interactiveTwo names, one object

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)
  • dest is 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: "maya@sky.dev" };
let prefs = { theme: "dark" };

// copy all properties from both sources into member
Object.assign(member, contact, prefs);

// member is now { name: "Maya", email: "maya@sky.dev", theme: "dark" }
alert(member.name);  // Maya
alert(member.email); // maya@sky.dev
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.

member
name: “Maya”
stats: •
clone
name: “Maya”
stats: •
{ stats }
height: 178
weight: 74
A shallow copy duplicates the top level but shares the nested stats object.

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
member
stats: •
weight: 74
clone
stats: •
weight: 74
A deep clone duplicates the nested objects too — nothing is shared.

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:

interactiveShallow shares, deep is independent

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.