In JavaScript, objects are handed around by reference rather than by value. That one rule explains why a function can quietly change an object you did not know it had touched, and why a copy that looks right on the surface still shares its innards with the original.
Why do two variables change the same object?
Assigning one variable to another does not create a second object. It creates a second name for the first one.
const settings = { theme: 'dark' };
const alias = settings;
alias.theme = 'light';
console.log(settings.theme); // light
console.log(settings === alias); // true
The two names are two handles on the same object, which is why the strict equality check says true and why changing one changes the other. Exactly the same thing happens when an object is passed into a function: the parameter is simply another name for the object the caller holds, so any change the function makes is visible to the caller afterwards.
What does a shallow copy actually copy?
A shallow copy creates a new top-level object, but it keeps the same references to the values inside it. One level of copying, no more.
const original = { name: 'Ada', prefs: { fontSize: 14 } };
const shallow = { ...original };
shallow.name = 'Grace'; // top level: the copy owns this key
shallow.prefs.fontSize = 18; // nested: both objects share one prefs
console.log(original.name); // Ada
console.log(original.prefs.fontSize); // 18
Look at the two results. Changing shallow.name left the original alone, because a string is copied by value. Changing shallow.prefs.fontSize did reach the original, because original.prefs and shallow.prefs point at the same nested object. Shallow copies are safe for flat data and unsafe the moment a value is itself an object.
One habit worth adopting: when data arrives from outside your program — a fetch response, a form value, a value parsed out of local storage — treat it as read-only. Freeze it with Object.freeze, or copy it with structuredClone before you start changing it. That converts a subtle reference bug into an immediate, obvious error at the point where the change is attempted.
How do I make a genuine deep copy?
structuredClone() is built into the platform and copies the entire structure — nested objects, nested arrays, maps, sets and dates included.
const original = { name: 'Ada', prefs: { fontSize: 14 } };
const deep = structuredClone(original);
deep.prefs.fontSize = 18;
console.log(original.prefs.fontSize); // 14
console.log(deep.prefs.fontSize); // 18
Now the two objects are genuinely independent. structuredClone also handles the things a hand-written recursive copy usually gets wrong: it preserves Date objects, Maps and Sets; it follows cyclic references instead of looping forever; and it refuses to copy functions, raising a DataCloneError rather than silently dropping them.
When you only need to change a couple of top-level fields, building a new object with spread is often better than cloning: it states exactly what you are keeping and what you are replacing, and it costs nothing. Reach for structuredClone when the structure is deep, when you do not know how deep it goes, or when the object came from outside your program and you need a genuinely private version of it.
What does a JSON round trip lose?
The older trick for cloning an object is to serialise it to JSON and parse the text back. It works for plain data and quietly loses everything JSON cannot represent.
const snapshot = JSON.parse(JSON.stringify({
when: new Date(0),
tags: ['a'],
missing: undefined,
set: new Set([1]),
}));
console.log(snapshot.when); // '1970-01-01T00:00:00.000Z'
console.log(snapshot.tags); // [ 'a' ]
console.log('missing' in snapshot); // false
console.log(snapshot.set); // {}
A Date turns into an ISO string, undefined disappears entirely, and a Set becomes an empty object because JSON has no way to describe one. The JSON round trip remains the right tool when the goal is moving data between systems, and the wrong tool when the goal is cloning an object inside your own program.
What happens to objects stored inside arrays?
The same rules apply, one level deeper. Spreading an array gives you a new array whose items are the very same objects, so changing an item inside the copy changes the item in the original.
const team = [{ name: 'Ada' }, { name: 'Linus' }];
const copied = [...team];
copied[0].name = 'Grace';
console.log(team[0].name); // Grace
const deep = structuredClone(team);
deep[0].name = 'Chen';
console.log(team[0].name, deep[0].name); // Grace Chen
Read the last line carefully. The deep copy was taken after the first change, so it inherited Grace before it was set to Chen. The point of the example is that the deep copy and the original no longer share anything: changing deep[0] leaves team[0] alone.
How do you compare two objects?
Two distinct objects are never equal under ===, because === compares references for objects. Comparing data therefore means comparing contents.
const original = { name: 'Ada' };
const copy = { ...original };
console.log(original === copy); // false
console.log(JSON.stringify(original) === JSON.stringify(copy)); // true
The first line is the rule itself. The second is the quick content check, and it works because JSON.stringify produces the same text for two objects with the same keys in the same order. Treat it as a debugging aid rather than a test: key order changes the output, and properties whose value is undefined are skipped entirely. For proper comparisons, Node's test runner ships deep equality assertions, and the testing module later in this tutorial shows how to use them.
Which copy technique should I use?
| Technique | Copies nested objects? | Keeps Date, Map, Set? | Note |
|---|---|---|---|
{ ...obj } | No | Yes, same references | One level only |
Object.assign({}, obj) | No | Yes, same references | Same depth as spread |
structuredClone(obj) | Yes | Yes | Throws on functions |
JSON.parse(JSON.stringify(obj)) | Yes | No | Loses Dates, Sets and undefined |
Try it yourself: write a file called copies.js and create const original = { title: 'Notes', tags: ['js'], meta: { pinned: false } };. Make a shallow copy with the spread syntax, push 'web' onto copy.tags, then print original.tags to watch it change. Now make a deep copy with structuredClone, push 'web' onto deep.tags and set deep.meta.pinned to true, then print original.tags and original.meta.pinned to watch them stay put. Finish by printing whether original === copy and whether original === deep, and by running JSON.stringify over an object containing a Date to see the string it produces.