JavaScript Value vs Reference: Copying and Shallow Copies
Understand primitive vs reference values in JavaScript, why copying objects surprises beginners, and how to make shallow and deep copies safely.
What you will learn
Have you ever copied an object, changed the copy, and then found that the original also changed? This is one of the most common surprises in JavaScript, and it all comes from one idea: values and references. In this lesson you will learn the difference between primitive values and reference values, how copying works for each, what happens when you pass them to functions, how to compare them, and how to make shallow copies and deep copies correctly.
Two kinds of data
JavaScript values come in two groups:
| Group | Types | Stored as |
|---|---|---|
| Primitive | string, number, boolean, null, undefined, bigint, symbol |
The value itself |
| Reference (objects) | objects, arrays, functions, dates, Map, Set… |
A reference (an address) pointing to the object |
Think of it like this:
- A primitive is like writing a number on a sticky note. If you give someone a copy, they get their own sticky note.
- An object is like a shared Google Doc. If you copy the link, both of you open the same document.
Primitives are copied by value
let a = 10;
let b = a; // b gets its own copy of 10
b = 20;
console.log(a); // 10 (unchanged)
console.log(b); // 20
Changing b has no effect on a. They are fully separate.
This is true for every primitive, including strings:
let name1 = "Riya";
let name2 = name1;
name2 = "Karan";
console.log(name1); // "Riya"
console.log(name2); // "Karan"
Good to know: Primitives are immutable. A string method like
toUpperCase()never changes the original string. It returns a new one.
Objects are copied by reference
const user1 = { name: "Riya" };
const user2 = user1; // copies the REFERENCE, not the object
user2.name = "Karan";
console.log(user1.name); // "Karan"
console.log(user2.name); // "Karan"
There is only one object in memory. Both user1 and user2 point to it, so a change through one name is visible through the other.
The same thing happens with arrays:
const list1 = [1, 2, 3];
const list2 = list1;
list2.push(4);
console.log(list1); // [1, 2, 3, 4]
console.log(list2); // [1, 2, 3, 4]
Reassigning is different from changing
This is an important detail. Changing the object affects everyone who shares it. Reassigning a variable only changes that one variable:
let first = { score: 10 };
let second = first;
second = { score: 99 }; // second now points to a NEW object
console.log(first.score); // 10 (first still points to the old object)
console.log(second.score); // 99
const does not mean “unchangeable”
const settings = { theme: "dark" };
settings.theme = "light"; // allowed!
console.log(settings); // { theme: "light" }
// settings = {}; // TypeError: Assignment to constant variable
const protects the variable (it cannot point somewhere else). It does not protect the contents of the object.
Passing to functions
When you pass a value to a function, JavaScript copies whatever is in the variable:
- For a primitive, that is the value, so the function gets its own copy.
- For an object, that is the reference, so the function can change the same object.
function addOne(number) {
number = number + 1;
return number;
}
let count = 5;
addOne(count);
console.log(count); // 5 (not changed)
function rename(person) {
person.name = "Changed"; // changes the shared object
}
const me = { name: "Riya" };
rename(me);
console.log(me.name); // "Changed"
But if the function reassigns its parameter, the outside variable is not affected:
function replace(person) {
person = { name: "Brand new" }; // only the local parameter changes
}
const me = { name: "Riya" };
replace(me);
console.log(me.name); // "Riya"
Remember: JavaScript always passes a copy of what is in the variable. For objects, that copy is a reference to the same object. Some people call this “pass by sharing”.
Comparing values and references
Primitives are compared by value:
console.log(5 === 5); // true
console.log("hi" === "hi"); // true
Objects are compared by reference (are they the very same object?):
const a = { x: 1 };
const b = { x: 1 };
const c = a;
console.log(a === b); // false (two different objects, even with the same content)
console.log(a === c); // true (same reference)
console.log([1, 2] === [1, 2]); // false
So === does not check the contents of objects. To compare contents, you need to compare property by property (or, for simple data, compare JSON strings).
const p1 = { x: 1, y: 2 };
const p2 = { x: 1, y: 2 };
console.log(JSON.stringify(p1) === JSON.stringify(p2)); // true (works for simple data only)
Making a copy: shallow copies
To get a separate object, you must create a new one. The easiest way is a shallow copy: a new outer object (or array) that contains the same top-level values.
Objects: spread and Object.assign
const user = { name: "Riya", age: 22 };
const copy1 = { ...user };
const copy2 = Object.assign({}, user);
copy1.name = "Karan";
console.log(user.name); // "Riya" (safe)
console.log(copy1.name); // "Karan"
Arrays: spread, slice and Array.from
const numbers = [1, 2, 3];
const copyA = [...numbers];
const copyB = numbers.slice();
const copyC = Array.from(numbers);
copyA.push(4);
console.log(numbers); // [1, 2, 3] (safe)
console.log(copyA); // [1, 2, 3, 4]
Copy and change in one step
Spread is great for “same object, but with one change”:
const user = { name: "Riya", age: 22, city: "Delhi" };
const updated = { ...user, city: "Mumbai" };
console.log(user.city); // "Delhi" (original untouched)
console.log(updated.city); // "Mumbai"
The shallow copy problem
A shallow copy copies only the first level. If a property holds another object or array, the reference is copied, so the original and the copy still share that inner object:
const original = {
name: "Aman",
address: { city: "Delhi" }
};
const shallow = { ...original };
shallow.name = "Neha"; // top level: safe
shallow.address.city = "Mumbai"; // nested: shared!
console.log(original.name); // "Aman" (safe)
console.log(original.address.city); // "Mumbai" (changed by mistake!)
The same happens with an array of objects:
const users = [{ name: "Riya" }, { name: "Karan" }];
const copy = [...users];
copy[0].name = "Changed";
console.log(users[0].name); // "Changed" (the objects inside are shared)
Fix 1: copy the nested part too
const original = { name: "Aman", address: { city: "Delhi" } };
const safe = {
...original,
address: { ...original.address } // new inner object
};
safe.address.city = "Mumbai";
console.log(original.address.city); // "Delhi" (safe)
Fix 2: a deep copy with structuredClone
A deep copy copies every level. The modern, built-in way is structuredClone:
const original = {
name: "Aman",
address: { city: "Delhi" },
hobbies: ["cricket", "coding"]
};
const deep = structuredClone(original);
deep.address.city = "Mumbai";
deep.hobbies.push("music");
console.log(original.address.city); // "Delhi"
console.log(original.hobbies); // ["cricket", "coding"]
structuredClone can also copy Date, Map, Set and even circular references. But it cannot copy functions (it throws an error), and it drops class methods.
The old trick: JSON.parse(JSON.stringify(x))
const deep = JSON.parse(JSON.stringify(original));
It works for plain data, but it has limits:
- Functions and
undefinedvalues are lost. Dateobjects become strings.NaNandInfinitybecomenull.MapandSetbecome empty objects.- Circular references throw an error.
Prefer structuredClone when it is available.
Frozen objects are also shallow
Object.freeze stops changes to an object, but only at the first level:
const config = Object.freeze({
mode: "dark",
limits: { max: 10 }
});
config.mode = "light"; // ignored
config.limits.max = 999; // allowed! (the inner object is not frozen)
console.log(config.mode); // "dark"
console.log(config.limits.max); // 999
Real-life use cases
1. Updating state without changing the old one
Modern tools like React expect you to create new objects instead of changing old ones:
const state = { user: "Riya", cart: ["pen"] };
const newState = {
...state,
cart: [...state.cart, "book"]
};
console.log(state.cart); // ["pen"]
console.log(newState.cart); // ["pen", "book"]
2. Default settings that must not be shared
const defaultSettings = { theme: "light", fontSize: 16 };
function createSettings(overrides) {
return { ...defaultSettings, ...overrides };
}
const mine = createSettings({ theme: "dark" });
console.log(mine); // { theme: "dark", fontSize: 16 }
console.log(defaultSettings); // { theme: "light", fontSize: 16 } (safe)
3. A function that must not change its input
function addItem(cart, item) {
return [...cart, item]; // return a new array
}
const cart = ["pen"];
const newCart = addItem(cart, "book");
console.log(cart); // ["pen"]
console.log(newCart); // ["pen", "book"]
4. A bug caused by sharing: the same default object
// Wrong: every student shares ONE marks array
const base = { marks: [] };
const s1 = { ...base, name: "Riya" };
const s2 = { ...base, name: "Karan" };
s1.marks.push(90);
console.log(s2.marks); // [90] (Karan got Riya's marks!)
// Right: create a fresh array for each student
function createStudent(name) {
return { name, marks: [] };
}
const t1 = createStudent("Riya");
const t2 = createStudent("Karan");
t1.marks.push(90);
console.log(t2.marks); // []
5. Filling an array with the same object
// Wrong: all items are the SAME object
const bad = new Array(3).fill({ done: false });
bad[0].done = true;
console.log(bad.map((t) => t.done)); // [true, true, true]
// Right: make a new object for each item
const good = Array.from({ length: 3 }, () => ({ done: false }));
good[0].done = true;
console.log(good.map((t) => t.done)); // [true, false, false]
6. Undo feature: save a snapshot
const editor = { text: "Hello", style: { bold: false } };
const snapshot = structuredClone(editor); // save the current state
editor.text = "Hello World";
editor.style.bold = true;
// Undo: go back to the snapshot
console.log(snapshot); // { text: "Hello", style: { bold: false } }
Common mistakes
- Thinking
const copy = originalmakes a copy. It only copies the reference. Use{ ...original }or[...original]. - Thinking a spread copy is fully independent. It is shallow. Nested objects and arrays are still shared.
- Expecting
===to compare the contents of objects.{ a: 1 } === { a: 1 }isfalse. - Thinking
constmakes an object unchangeable. It only stops reassigning the variable. - Changing a function’s parameter object by accident. Return a new object or array instead.
- Using
fill({})orfill([])to create many separate items. They all share one object. UseArray.from. - Using
JSON.parse(JSON.stringify(x))on data with dates, functions orundefined. UsestructuredClone. - Thinking
Object.freezeis deep. It only freezes the first level.
Practice
- Make
let x = 5; let y = x; y = 10;and print both. Explain whyxdid not change. - Create
const a = { n: 1 }; const b = a; b.n = 2;and printa.n. Explain the result. - Copy the array
[1, 2, 3]in three different ways and change the copy. Check that the original did not change. - Create an object
{ name, address: { city } }. Make a shallow copy, change the city in the copy, and see what happens to the original. - Repeat the same test with
structuredCloneand compare the results. - Write a function
renameUser(user, newName)that returns a new user object without changing the old one. - Create 3 separate
{ done: false }objects in an array usingArray.from. - Check what
{ a: 1 } === { a: 1 }gives, and write a small functionisSame(obj1, obj2)that compares simple objects withJSON.stringify. - Challenge: write your own
deepCopy(value)function using recursion that works for nested objects and arrays.
Recap
- Primitives (string, number, boolean,
null,undefined, bigint, symbol) are copied by value. Each variable has its own copy. - Objects (objects, arrays, functions…) are copied by reference. Both variables point to the same object.
- Changing a shared object affects every variable pointing to it. Reassigning a variable affects only that variable.
constprotects the variable, not the contents of an object.- Functions receive a copy of the variable’s content. For objects, that is a reference, so a function can change the original object.
===compares primitives by value and objects by reference.- Make a shallow copy with
{ ...obj },Object.assign({}, obj),[...arr],arr.slice()orArray.from(arr). It copies only the first level. - Make a deep copy with
structuredClone(value). The JSON trick works only for plain data. - Next you will learn optional chaining and nullish coalescing, two small operators that make working with nested objects much safer.