Assignment copies the reference, not the object
This lesson exists because the most confusing bug class in JavaScript is two variables secretly sharing one object. The rule behind it is short: a variable never contains an object directly, it contains a reference, which is the object's address in memory.
So const b = a copies the address, not the data. Afterward a and b point at the same object, and a change made through either name is visible through the other, because there is only one object to change.
Numbers, strings, and booleans do not behave this way, since assigning one of those copies the value itself. Only objects and arrays are shared by reference, and arrays count here because an array is a kind of object.
The same rule explains what === does with objects. It compares references, asking whether both sides are the same object in memory, and it never looks at the contents. That is why { a: 1 } === { a: 1 } is false: those are two separate objects that merely look alike.
To copy for real, build a new object or array. { ...obj } and [...xs] spread the contents into a fresh one. One warning comes with that: spread makes a shallow copy, only one level deep, so objects nested inside are still shared. For a fully independent copy of nested data, use structuredClone(obj).
Python lists and dicts behave identically, so you may have met this bug before under the name aliasing.
Two names for one object, and two objects that match
The first half of this program shows sharing, and the last line shows the flip side, two genuinely separate objects that === still refuses to call equal.
const a = { score: 1 }; const b = a; // copies the reference, NOT the object b.score = 99; console.log(a.score); console.log(a === b); console.log({ score: 1 } === { score: 1 });
Output
99 true false
a.score reports 99 even though nothing was ever assigned through a, because b = a copied only the arrow and both names now lead to one object. That is also why a === b is true: same address, same object.
The last line builds two fresh objects with identical contents, and === answers false. Identical contents are irrelevant to the comparison, which asks only whether the two references point at the same place. Comparing objects by value takes a field-by-field check or a helper written for that purpose.
Fixing a settings object that shares by accident
This program carries the classic version of the bug. userSettings was created with =, so it is not a copy at all, and raising its font size also raises the font size in defaults. Both printed lines report 18 when they should report 14 and 18.
const defaults = { theme: "dark", fontSize: 14 }; const userSettings = defaults; // BUG: this shares, it does not copy userSettings.fontSize = 18; console.log(defaults.fontSize); console.log(userSettings.fontSize);
Spreading the defaults into a new object fixes it, and only that one line changes:
const defaults = { theme: "dark", fontSize: 14 }; const userSettings = { ...defaults }; userSettings.fontSize = 18; console.log(defaults.fontSize); console.log(userSettings.fontSize);
Output
14 18
{ ...defaults } builds a brand new object and pours every property of defaults into it, so the two objects have matching contents and separate identities. From that point on, editing one has no effect on the other.
This pattern of defaults plus overrides shows up in configuration code constantly, and it is exactly where accidental sharing does the most damage, because the corrupted defaults then leak into every later use.
Pushing through a second name
Given const a = [1, 2]; followed by const b = a; and b.push(3);, the value of a.length is 3.
b = a copied the reference, so a and b name the same array, and a push through either name is visible through both. The array never split into two.
The const on both declarations changes nothing here, because const prevents reassigning a variable and not changing the array it points at, which is the same rule lesson 4-2 covered when push worked on a const array.
| Operation | Copies the value | Copies the reference |
|---|---|---|
let n2 = n1 on a number | ✓ | |
let s2 = s1 on a string | ✓ | |
const b = a on an array or object | ✓ | |
const b = [...a] or { ...a } | ✓ one level deep | |
const b = structuredClone(a) | ✓ all levels |