Values, references, and copies
The foundational JavaScript mental model of how variables bind to values.
Picture this scenario: A teammate’s pull request adds code that passes the user object to an analytics helper function. The update ships, and within an hour two pieces of UI on the dashboard start drifting out of sync. The sidebar shows the old name, the header shows the new one, and only a refresh brings them back together. The cause turns out to be ordinary: the helper changed the same object the calling component still held a reference to. This bug comes from treating = as if it duplicates a value. What = actually does is bind a name to a value . By the end of this lesson you’ll know exactly which values are shared and which aren’t.
How = treats primitives and objects
Section titled “How = treats primitives and objects”How = behaves in JavaScript depends on which of the following two categories the value belongs to:
Primitives: string, number, boolean, bigint, symbol, null, and undefined. When you assign a primitive to a name and then assign that name to another name, the value itself is copied, and the two names are independent.
Objects (including arrays and functions). When you assign an object to a name, the name holds a reference to the object, which is a small pointer-like value that records where the object lives. If you assign that name to another name, what gets copied is the reference, so both names will point at the same object.
The right panel is where people get it wrong. If user and alias both point at the same object, a change made through one name shows up when you read through the other:
const user = { name: 'Ada', age: 36 };const alias = user;
alias.name = 'Grace';console.log(user.name); // 'Grace'What a function shares with its caller
Section titled “What a function shares with its caller”A function call binds names just like = does. Each parameter is a new name inside the function: for a primitive, the value itself is copied; for an object, the reference is copied, so the parameter points at the same object.
Compare these two ways a function can try to modify a user:
function rename(user: { name: string; age: number }) { user = { name: 'Grace', age: 36 };}
const ada = { name: 'Ada', age: 36 };rename(ada);console.log(ada.name); // 'Ada'Inside the function, the parameter user is rebound to a new object, which only affects the function’s local name. ada still points at the original object, which was never modified.
function rename(user: { name: string; age: number }) { user.name = 'Grace';}
const ada = { name: 'Ada', age: 36 };rename(ada);console.log(ada.name); // 'Grace'Inside the function, user and ada both point at the same object. The function doesn’t rebind anything; it reaches through its local reference and changes a property on the shared object. That change is visible everywhere the object is held.
Together the two tabs give you the rule. A function that reassigns its parameter is invisible to the caller, while a function that changes a property on the object the parameter points at is visible to the caller. You don’t need to memorize this: once you can picture the diagram in your head, you can work out the behavior at the call site step by step.
Shallow copy: the spread operator
Section titled “Shallow copy: the spread operator”To modify a value without the change leaking back through every reference that points at it, use the spread operator to copy it first, then modify the copy:
const next = { ...prev };const nextItems = [...items];For objects, { ...prev } builds a new object with the same properties. For arrays, [...items] builds a new array with the same elements.
“shallow” means the spread operator copies the top level, but any nested object will be referenced. The exercise below lets you try this yourself:
Predict what each binding holds after the spread copy and the two mutations. Replace each null with your prediction (a string), then run the tests.
Reveal the answer
const prediction = { originalName: 'Ada', copyName: 'Grace', originalStreet: 'Babbage Ave', copyStreet: 'Babbage Ave',};The spread copied the top level, so renaming copy.name left original.name as Ada, but address was a reference, and the spread copied that reference rather than the object behind it. Both original.address and copy.address now point at the same nested object, so writing to copy.address.street writes to it for original too.
When you need to copy an object, a shallow copy should be your first choice. Most objects you’ll copy are flat or only one level deep, so a spread is enough to keep your changed object properties from leaking out.
Deep copy: structuredClone
Section titled “Deep copy: structuredClone”To prevent changes in nested objects from leaking, you can use structuredClone, a global function available in Node and the browser (requires no import).
const original = { name: 'Ada', address: { street: 'Lovelace Ln', city: 'London' },};
const copy = { ...original };copy.address.street = 'Babbage Ave';
console.log(original.address.street); // 'Babbage Ave'The spread copied the top level, but address is still shared. Writing through copy.address changes the original too.
const original = { name: 'Ada', address: { street: 'Lovelace Ln', city: 'London' },};
const copy = structuredClone(original);copy.address.street = 'Babbage Ave';
console.log(original.address.street); // 'Lovelace Ln'structuredClone walks the whole structure and copies every nested object on its own. No shared reference is left between original and copy, so changes stay where you make them.
Since structuredClone has to walk the object’s structure, its cost is higher, so use it only when the data is genuinely nested. structuredClone also handles cyclical references, where an object contains a reference back to itself and it preserves types like Date, Map, Set, ArrayBuffer, typed arrays, and RegExp.
External resources
Section titled “External resources”Step through any of the snippets in this lesson and watch the bindings, references, and heap update line by line.
MDN's canonical guide to the seven primitives, the object category, and how typeof and equality interact with them.
The full list of cloneable types, transferables, and the exact DataCloneError conditions you may hit in production.
Surma's web.dev article on why structuredClone replaced the JSON.parse(JSON.stringify(x)) workaround, with concrete edge cases.