Start.dev
Back to blog

Value vs reference in JavaScript, explained with examples

You copied an array, changed the copy, and the original changed too. Here is what a JavaScript variable actually holds and how to copy safely.

Ariel Weinberger
Ariel Weinberger ยท 6 min read

JavaScript passes arguments by value. The part that confuses people is what the value is. For a number, a string, or a boolean, the variable holds the value itself. For an object or an array, the variable holds a reference to the object, not the object, and that reference is what gets copied when you assign to another variable or pass it to a function. So two variables can point at the same object, and a change made through one shows up in the other.

That is the whole explanation for the bug where you copy an array, change the copy, and the original changes too. Here is how it plays out, with code you can run.

Primitives are copied, objects are shared

Assigning a primitive copies the value. Afterwards the two variables have nothing to do with each other:

let a = 1
let b = a
b = 2
console.log(a, b) // 1 2

Assigning an object or an array copies the reference. Both variables now point at the same thing:

const list = ["Milk", "Bread"]
const copy = list
 
copy.push("Eggs")
 
console.log(list)          // [ 'Milk', 'Bread', 'Eggs' ]
console.log(list === copy) // true

There was never a second array, only a second name for the same one.

The primitives are string, number, bigint, boolean, undefined, symbol, and null. MDN's glossary entry notes that they cannot be changed, only replaced. Everything else, including arrays and functions, is an object and is held by reference. The What Is A Data Type (opens in a new tab) lesson in JavaScript Basics introduces the two groups.

Passing an object to a function

When you call a function with an object, the parameter receives a copy of the reference. Reassigning the parameter replaces that copy and leaves the caller's variable alone. Changing a property goes through the reference to the one shared object, and the caller sees it:

function rename(user) {
  user = { name: "Grace" }
}
 
function renameInPlace(user) {
  user.name = "Grace"
}
 
const ada = { name: "Ada" }
 
rename(ada)
console.log(ada.name) // Ada
 
renameInPlace(ada)
console.log(ada.name) // Grace

That first call is why JavaScript is described as passing by value. If it passed by reference the way C++ or PHP can, rename would have changed ada. It did not. MDN's functions guide explains it with the same two cases. The rule to remember: reassigning a parameter changes nothing outside the function, and changing the object it points to changes it everywhere.

const does not stop changes to the object

const makes the variable name permanent. It says nothing about the object the name points to:

const config = { theme: "dark" }
config.theme = "light"
console.log(config.theme) // light

What const prevents is config = somethingElse. If you need an object that never changes, you arrange that yourself, usually by never editing it and making new objects instead. More on that below.

A copy is one level deep unless you ask for more

Spread and Object.assign create a new outer object and copy the top-level properties into it. A property that holds an object is copied as a reference, so the nested object is still shared:

const user = { name: "Ada", address: { city: "London" } }
const copy = { ...user }
 
copy.name = "Grace"
copy.address.city = "Paris"
 
console.log(user.name)         // Ada
console.log(user.address.city) // Paris

The first change stayed in the copy. The second one leaked, because copy.address and user.address are the same object. MDN's spread syntax reference calls this a shallow copy, and the Spread (opens in a new tab) lesson has you make one and then update it.

When the nested objects need copying too, use structuredClone:

const original = { name: "Ada", address: { city: "London" } }
const deep = structuredClone(original)
 
deep.address.city = "Paris"
console.log(original.address.city) // London

It copies plain data, arrays, dates, maps, and sets. It cannot copy functions, so use it for data, not for objects that carry behavior.

The same rule applies to arrays. map, filter, slice, and spread give you a new array, while push, splice, sort, and reverse change the one you already have. sort catches people because it returns the array it just reordered, which looks like a copy but is not. The Sort (opens in a new tab) lesson in Intermediate JavaScript works through that case.

Two objects are equal only when they are the same object

=== compares primitives by value and objects by reference. Two objects with identical contents are still two different objects:

console.log({ a: 1 } === { a: 1 }) // false
 
const x = { a: 1 }
const y = x
console.log(x === y) // true

This is the same fact again. The variable holds a reference, so === asks whether two references point at the same object. To compare contents, compare the fields yourself or use a library that does. MDN's strict equality reference has the full rules, and the Comparison And Equality (opens in a new tab) lesson covers the primitive side.

Why React wants a new object

React decides whether to re-render by comparing the previous state to the next one with Object.is, which is reference comparison. If you change a property on the state object and then set it, React sees the same reference and does nothing. If you build a new object, the references differ and the component updates:

let state = { count: 0 }
 
const before = state
state.count = 1
console.log(Object.is(before, state)) // true, React would skip this update
 
const next = { ...state, count: 2 }
console.log(Object.is(state, next))   // false, React would re-render

That is why the React docs on updating objects in state and updating arrays in state tell you to use spread, map, and filter instead of push and property assignment. The Updating Objects In State (opens in a new tab) and Updating Arrays In State (opens in a new tab) lessons practice both.

Does this change the original?

Decide for each one before you look at the answers.

  1. const b = a; b.push(4) where a is an array
  2. const d = [...c]; d.push(4) where c is an array of numbers
  3. const shallow = [...people]; shallow[0].name = "Grace" where people is an array of objects
  4. function bump(o) { o.count += 1 }; bump(counter)
  5. const sortedCopy = nums.slice().sort()
  6. const clone = structuredClone(cart); clone.items.push(2)
CaseChanges the original?Why
1Yesb is the same array as a.
2NoSpread made a second array, and numbers are copied.
3YesThe new array holds the same object references, so shallow[0] is people[0].
4Yeso points at counter, and += changes a property on that object.
5Noslice() copied first, so sort() sorted the copy.
6NostructuredClone copied the nested array too.

If one of these surprised you, the sentence to go back to is the first one in this post: for objects, the value being copied is a reference.

If you want to practice this rather than read about it, the JavaScript track covers it in order, from primitives and spread in JavaScript Basics, through array methods in Intermediate JavaScript and readonly in TypeScript Fundamentals, to making a new value instead of editing the old one in Software Design Principles:

Two habits that avoid the bug

Inside a function, do not change objects you did not create. If the function's job is to produce an updated version of something, return a new object and leave the argument alone.

When state is shared, in React, in a store, or in a module-level variable, change it by making a new value rather than editing the old one. The Change By Making A New One (opens in a new tab) lesson in Software Design Principles goes through why that makes changes traceable.

When a bug looks like a variable changed on its own, ask what the variable holds. If it holds a primitive, only an assignment to that name could have changed it. If it holds a reference, some other variable, parameter, or array slot holds the same reference, and that is where the change came from. The same habit of asking what a piece of code holds is behind two earlier posts: expressions versus statements asks what value a line produces, and closures asks which variables a function still holds on to.

Level up your coding skills

Jump into interactive courses and start building real-world projects.

Start learning

Related posts

You can read JavaScript but keep forgetting how to write it. The difference between an expression and a statement explains most of the syntax.
Ariel Weinberger
Ariel Weinberger ยท 11 min read
Learn JavaScript closures with tested examples: counters, the var vs let loop question, private state, debounce, and the stale closure bug in React hooks.
Brad Traversy
Brad Traversy ยท 8 min read
What useEffect really does, when it runs, and how the dependency array and cleanup function work. Simple examples for fetching data, timers and page titles.
Ariel Weinberger
Ariel Weinberger ยท 5 min read