MI. All notes

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.

Nothing 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);
Pick the output before revealing it.

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);
What does person.name contain?
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.

Useful rule: if a function shouldn’t surprise its caller, return a new object or array instead of editing the one it received.

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);
Does the spread protect the original name?

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" }false

Two object literals create two objects.

const b = a;
a === b
true

Both variables lead to one object.

[] === []false

Each array literal creates a new array.

const second = first;
first === second
true

Both 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.

Usually leaves the original alonemap · filter · slice · concat
Changes the original arraypush · pop · splice · sort · reverse

Once 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.

What assignment copies

Primitive value

x7
y7
Two independent values

Object value

a
b
one shared object
Two ways to reach it
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?