July 14, 2026 · JavaScript · 12 min read
A Visual Guide to References in JavaScript
Common beliefAssigning an object to a second variable makes a second object.
It feels true because assignment looks like copying. Then you change the “copy,” and the original changes too. If you’ve ever asked, “Why did that value change when I never touched it?”, you’ve met a reference bug.
We’ll start with normal variables, build one idea at a time, and finish with the bugs this causes in real JavaScript.
01 / See it before naming it
One object. Two ways to reach it.
Run the three lines. Watch whether JavaScript creates one object or two.
Variable names
Object
score0Nothing has run yet.
The lines are a drawing aid. JavaScript doesn’t show you arrows in your code.
Line 1 creates one object. The name profile gives us a way to find it.
Line 2 does not create another object. It gives us another way to find the same one. Now both profile and backup lead to a single object.
Line 3 changes that object. It doesn’t matter which name we used to reach it. There’s only one score, so both names will see 1.
Why doesn’t assignment behave this way when the value is a number or a string?
02 / Start with ordinary values
A variable is a name for a value.
When you write let score = 7, JavaScript connects the name score to the value 7. The variable isn’t the value itself. It’s the name your code uses to get that value.
Now copy it:
let score = 7;
let copy = score;
score = 10;
copy received the value 7. Later, score = 10 only points the name score at a different value. It doesn’t create a live connection between score and copy.
Go deeper: which values are primitive?
Strings, numbers, booleans, null, undefined, bigint, and symbols are called primitive values. A primitive can’t be edited from the inside. You can only replace it with another value.
That’s why name.toLowerCase() returns a new string. It can’t reach inside the old string and change one letter.
Objects are different because they have an inside. So what exactly can change?
03 / Two actions that look similar
Replacing a variable is not changing an object.
This is the fork in the road. Most reference confusion comes from mixing up these two actions:
Reassign the variable
let book = { checkedOut: false };
book = { checkedOut: true };
book now leads to a new object. The old object wasn’t edited.
Change the object
let book = { checkedOut: false };
book.checkedOut = true;
book still leads to the same object. A property inside it changed.
Changing a value inside an object is called mutation. The word sounds dramatic, but it just means “edit the existing object.”
Arrays can be mutated too. Methods like push, pop, and sort edit the existing array.
If two variables lead to that object, a mutation is visible through both variables. Reassigning one variable is not.
You can now explain the first animation—but can you predict it without seeing the arrows?
04 / What const really protects
const locks the variable, not the object.
The word const sounds like “constant forever,” so this code can be surprising:
const settings = { theme: "light" };
settings.theme = "dark"; // allowed
This works because the variable settings still leads to the same object. We changed a property inside that object. We did not reassign the variable.
This version fails:
const settings = { theme: "light" };
settings = { theme: "dark" }; // TypeError
Now we’re trying to point settings at a different object. That is the part const prevents.
The same rule applies to arrays. A const array can still use push because the variable keeps leading to the same array. You just can’t replace it with another array.
const doesn’t stop shared changes. Can you now predict one without seeing the arrows?
05 / Check the model
Which value gets logged?
const a = { count: 0 };
const b = a;
b.count++;
console.log(a.count);
a and b lead to the same object. The ++ edits that object, so reading count through a returns 1.
Some developers say b is a reference to the object. That’s useful shorthand. It means b holds a value that lets JavaScript find that particular object.
You’ll also hear the word identity. An object’s identity is simply “which exact object is this?” Two objects may contain the same data and still be two different objects.
A function creates another variable name for its argument. Does changing that name leak back outside?
06 / References inside functions
A function can edit your object without returning it.
When you pass a value into a function, the parameter becomes a new local variable. JavaScript copies the value into that parameter.
For an object, the copied value still leads to the same object. That’s why a function can edit an object created somewhere else:
function rename(user) {
user.name = "Lin";
}
const person = { name: "Ada" };
rename(person);
console.log(person.name);
The local name user and the outside name person lead to one object. The function edits that shared object.
Go deeper: reassign the parameter instead
Reassigning the parameter is different:
function rename(user) {
user = { name: "Lin" };
}
const person = { name: "Ada" };
rename(person);
console.log(person.name); // "Ada"
user = newObject only changes the local variable. It points user at a new object. The outside variable person still leads to the original one.
If shared objects are the problem, can we ask JavaScript to make a real copy?
07 / Make a separate object
Copy the container, then change the copy.
For a simple object, the spread syntax creates a new object and copies the properties into it:
const book = { title: "Dune", checkedOut: false };
const copy = { ...book };
copy.checkedOut = true;
console.log(book.checkedOut); // false
console.log(copy.checkedOut); // true
Now book and copy lead to different objects. Changing one top-level property won’t change the other.
Arrays work the same way with square brackets:
const original = [3, 1, 2];
const copy = [...original];
copy.sort();
console.log(original); // [3, 1, 2]
console.log(copy); // [1, 2, 3]
Go deeper: other ways to copy
You can also use Object.assign({}, object) for objects and array.slice() for arrays. The spelling changes. The goal is the same: create a new outer container.
The spread looks like a full copy. But what happens when an object contains another object?
08 / The nested-object trap
Spread copies one level, not the whole tree.
A new outer object can still share an object nested inside it.
const state = { user: { name: "Ada" } };
const next = { ...state };
next.user.name = "Lin";
console.log(state.user.name);
state !== next, but state.user === next.user. The outer object was copied. The nested user object was shared.
This is called a shallow copy. “Shallow” means only the first level is new.
To update the name without touching the old state, copy every level on the path to the value you’re changing:
const next = {
...state,
user: {
...state.user,
name: "Lin"
}
};
Go deeper: what about structuredClone?
For a true deep copy of supported data, structuredClone(value) can help. It’s not a replacement for careful updates, and it can’t clone every kind of JavaScript value.
Shared objects also change the meaning of ===. What does JavaScript compare?
09 / Same contents, different object
Objects are equal only when they are the same object.
For primitives, === compares the values. Two separate strings containing "hello" are equal.
For objects, === asks whether both sides lead to the exact same object. It does not open the objects and compare every property.
{ name: "Ada" } === { name: "Ada" }falseTwo object literals create two objects.
const b = a;
a === btrueBoth variables lead to one object.
[] === []falseEach array literal creates a new array.
const second = first;
first === secondtrueBoth variables lead to one array.
Go deeper: function identity and event listeners
Functions follow the same rule because functions are objects. Two arrow functions with identical code are still different function objects.
(() => console.log("hi")) ===
(() => console.log("hi")); // false
This matters with event listeners. To remove a listener, keep the original function and pass that same function back:
const onClick = () => console.log("clicked");
document.addEventListener("click", onClick);
document.removeEventListener("click", onClick);
Arrays, functions, dates, maps, and sets all belong to JavaScript’s broad object family. Creating a new one gives it a new identity, even when its contents look identical.
References are invisible, so the hardest bugs appear when a familiar method edits shared data behind your back →
10 / A real-world surprise
Some array methods change the array you give them.
Imagine a helper that returns the smallest number:
function smallest(numbers) {
numbers.sort((a, b) => a - b);
return numbers[0];
}
const prices = [30, 10, 20];
const lowest = smallest(prices);
lowest is 10, which looks correct. But prices is now [10, 20, 30]. The function sorted the original array because numbers and prices lead to the same array.
The safe version copies before sorting:
function smallest(numbers) {
const sorted = [...numbers].sort((a, b) => a - b);
return sorted[0];
}
The same question applies to any function or library call: does it return a new value, or does it edit the value I passed in? Method names don’t always tell you.
map · filter · slice · concatpush · pop · splice · sort · reverseOnce you know whether data is shared, these “ghost changes” stop being mysterious. What should you remember tomorrow?
11 / The one-screen recap
Keep this mental picture.
Primitive value
Object value
Reassignment moves one name. Mutation changes the shared object.
1. Assignment copies a value.
2. An object value leads to a particular object.
3. Copying that value does not copy the object.
4. Mutation is visible through every variable that leads there.
5. Spread creates a new outer object, but nested objects may still be shared.
There’s one last place this picture pays off immediately: React state.
12 / Where React fits
React uses new objects as a change signal.
React is not the starting point for references. It’s one place where the same JavaScript rules become very visible.
If you mutate a state object, it keeps the same identity. React may not see the new object it expects. If you create a new object at the level you changed, React gets a clear signal and the old state stays untouched.
// Avoid editing the existing object
user.name = "Lin";
setUser(user);
// Give React a new object
setUser({ ...user, name: "Lin" });
Dependency arrays, memoized components, selectors, and caches also compare object identity. The framework didn’t invent these rules. It simply makes JavaScript’s reference behavior matter more often.
When a value changes unexpectedly, draw the names and objects. Which names still lead to the same thing?