The concept of "reference" and "value" in JavaScript

javascript5 min

byLucas Santos

This page was machine translated. Read original / Suggest a fix

During a conversation in the Formação TS community, one of the students asked about a topic that used to be pretty famous, but I myself haven’t heard anyone talk about it in years. The concept of reference vs value in JavaScript.

So I realized I’d never made any content about it and decided to write something more detailed on the subject.

What are ref and val?#

Reference and value are pretty old concepts. I remember Visual Basic had two keywords that let you choose whether the result would be a ByVal or a ByRef:

Public Sub ChangeFieldValue(ByVal cls As Class1)
cls.Field = 500
End Sub
Public Sub ChangeFieldReference(ByRef cls As Class1)
cls.Field = 500
End Sub

The idea is pretty simple:

  • References point to the original object like a pointer, so changes to any variable holding that pointer will change the original object too
  • Values don’t change the original object, because the variable’s value is a clone of the original value, not a pointer

But how does this work in JavaScript?

In JavaScript#

In JavaScript we call this crowd Reference Type when it’s a reference, or Value Type for values. In short, everything that’s a primitive type in JS is passed by value:

  • Numbers
  • Strings
  • Booleans

Whenever you have one of these three types, you get pass by value, meaning they get cloned whenever you send the variable from one place to another, for example:

let original = 10
function vezesDois (num) {
num *= 2
return num
}
const mutado = vezesDois(original)
console.log(mutado) // 20
console.log(original) // 10

Notice that even though we modify the value and reassign it to num, the original value stayed 10, because num is a copy of original.

Now what about references? This whole concept works a bit differently.

References#

Everything that isn’t a primitive in JS is treated as an object, and a lot of things in JS are objects:

  • Objects (obviously)
  • Functions
  • Arrays
  • null
  • RegExp
  • Classes

When you create an object in JS, whether as a literal or through a constructor, you’re creating a pointer to something called a hidden class (I have an entire article just about this, 10 of them actually). A hidden class is a value, the object you created points to that value, but we won’t get into the details here.

What matters is that whenever you have an object, and you pass that object to another variable or another function, what gets passed is the pointer, not the value itself. And this is extremely important, because any change to the pointer will change the original object.

🤔

Did you know? This is one of the reasons why we can use const x = [] and then modify the array even though it’s constant: we’re modifying the reference, not the pointer. But if we try to reassign x = [], that isn’t possible, because [] is a different object and a different pointer.

The most famous example of this is array methods like sort and reverse, which mutate the original array (along with push, pop, and plenty of others):

const original = [1, 2, 3, 4, 5]
function foo (arr: any[]) {
arr.reverse()
}
console.log(original) // [1, 2, 3, 4, 5]
foo(original)
console.log(original) // [5, 4, 3, 2, 1]

Comparison#

Another important point is comparisons using references. When we’re working with values, we can do something like this:

const a = 10
const b = 10
console.log(a===b) // true

Which is totally valid since a and b have the same value. But what if we do this instead:

const a = { nome: 'Lucas' }
const b = { nome: 'Lucas' }
console.log(a===b) // false

This trips up a lot of people, so many that I even made a tweet about it:

But the reality is that understanding this problem is pretty simple. Remember when we talked about pointers? Well, every time you create a new object with {} or with a constructor, you get a new pointer, imagine it’s something like this:

const a = {} // pointer: 0x89ac (example)
const b = {} // pointer: 0x1b3d
console.log(a === b) // 0x89ac === 0x1b3d? false

Of course pointers are a little different from what I just showed, but you get the idea. We can’t compare objects because the references are different, and the only way to know two objects are equal is if both references are equal:

const a = {} // pointer: 0x89ac (example)
const b = a // pointer: 0x89ac
console.log(a === b) // 0x89ac === 0x89ac? true

Cloning#

To get around this problem and avoid mutating the original variable, there’s the concept of cloning. This is actually a pretty interesting topic, since it was one of the subjects of the article about the new array methods here on the blog.

When we clone an object, we’re grabbing all the properties from the original object and dumping them into a different pointer, so that if we change this new object, we won’t touch the original variable. The most common way to do it used to be something like this:

const original = [1, 2, 3, 4, 5]
function foo (arr: any[]) {
const clone = Object.assign([], arr)
return clone.reverse()
}
console.log(original) // [1, 2, 3, 4, 5]
const clone = foo(original) // [5, 4, 3, 2, 1]
console.log(original) // [1, 2, 3, 4, 5]

Notice that the original array stayed the same. Over time we moved away from Object.assign and started using StructuredClone, which does the same thing but with a twist:

const original = [1, 2, 3, 4, 5]
function foo (arr: any[]) {
const clone = structuredClone(arr)
return clone.reverse()
}
console.log(original) // [1, 2, 3, 4, 5]
const clone = foo(original) // [5, 4, 3, 2, 1]
console.log(original) // [1, 2, 3, 4, 5]

The twist is that if you have an object with another object inside it, using Object.assign you’re only cloning the outer pointer, because the object itself has a pointer to another object inside it:

const nested = { c: 1 }
const objNested = {
a: {
b: nested
}
}
function modify(obj) {
obj.a.b.c = 10
}
console.log(objNested.a.b.c) // 1
modify(objNested)
console.log(objNested.a.b.c) // 10

And even with the clone, this doesn’t work:

const nested = { c: 1 }
const objNested = {
a: {
b: nested
}
}
function modify(obj) {
const clone = Object.assign({}, obj)
clone.a.b.c = 10
}
console.log(objNested.a.b.c) // 1
modify(objNested)
console.log(objNested.a.b.c) // 10

Because it’s as if we were reading this here:

const nested = { c: 1 } // pointer 0x1a
const objNested = { // pointer 0x5b
a: {
b: nested // objNested.a.b === c -> 0x1a === 0x1a -> true
}
}
function modify(obj) {
const clone = Object.assign({}, obj) // clone.a.b is 0x1a
clone.a.b.c = 10 // we're changing c's pointer here
}
console.log(objNested.a.b.c) // 1
modify(objNested)
console.log(objNested.a.b.c) // 10

To make it work, we’d have to write objNested.a.b = Object.assign({}, objNested.a.b.c), which is simple enough with one nested object, but gets messy fast when there are several. That’s why we can use StructuredClone instead.

Conclusion#

I hope this short article cleared up the main differences between references and values in JavaScript. There’s a lot more good content about objects out there, and I myself already wrote an article about prototypes that’ll shed some light on how JavaScript handles methods and inheritance under the hood!

See you!