A Visual Guide to References in JavaScript

By Dave Ceddia

a.k.a. “pointers for JavaScript developers”

On Day 1 of learning to code, someone tells you “A variable is like a box. Writing thing = 5 puts 5 in the thing box”. And that’s not really how variables work, but it’s good enough to get you going. It’s like in math class when they lie to you about the full picture, because the full picture would explode your brain right now.

Some time later though, you start to see weird problems. Variables changing when you didn’t change them. Ghosts in the machine.

“I thought I made a copy of that! Why did it change?” <– that right there is a reference bug!

By the end of this post, you’ll understand why that happens and how to fix it.

What is a Reference?

References are everywhere in JS, but they’re invisible. They just look like variables. Some languages, like C, call these things out explicitly as pointers, with their own syntax to boot. But JS doesn’t have pointers, at least not by that name. And JS doesn’t have any special syntax for them, either.

Take this line of JavaScript for example: it creates a variable called word that stores the string “hello”.

let word = "hello"

the word variable pointing at a box containing the string hello

Notice how word points to the box with the “hello”. There’s a level of indirection here. The variable is not the box. The variable points to the box. Let that sink in while you continue reading.

Now let’s give this variable a new value using the assignment operator =:

word = "world"

What’s actually happening here isn’t that the “hello” is being replaced by “world” – it’s more like an entirely new box is created, and the word is reassigned to point at the new box. (and at some point, the “hello” box is cleaned up by the garbage collector, since nothing is using it)

If you’ve ever tried to assign a value to a function parameter, you probably realized this doesn’t change anything outside the function.

The reason this happens is because reassigning a function parameter will only affect the local variable, not the original one that was passed in. Here’s an example:

function reassignFail(word) {
  // this assignment does not leak out
  word = "world"
}

let test = "hello"
reassignFail(test)
console.log(test) // prints "hello"

Initially, only test is pointing at the value “hello”.

Once we’re inside the function, though, both test and word are pointing at the same box.

Two variables with the same value

After the assignment (word = "world"), the word variable points at its new value “world”. But we haven’t changed test. The test variable still points at its old value.

After reassigning word

This is how assignment works in JavaScript. Reassigning a variable only changes that one variable. It doesn’t change any other variables that also pointed at that value. This is true whether the value is a string, boolean, number, object, array, function… every data type works this way.

Two Types of Types

JavaScript has two broad categories of types, and they have different rules around assignment and referential equality. Let’s talk about those.

Primitive Types in JavaScript

There are the primitive types like string, number, boolean (and also symbol, undefined, and null). These ones are immutable. a.k.a. read-only, can’t be changed.

When a variable holds one of these primitive types, you can’t modify the value itself. You can only reassign that variable to a new value.

The difference is subtle, but important!

Said another way, when the value inside a box is a string/number/boolean/symbol/undefined/null, you can’t change the value. You can only create new boxes.

It does not work like this…

This is why, for example, all of the methods on strings return a new string instead of modifying the string, and if you want that new value, you’ve gotta store it somewhere.

let name = "Dave"
name.toLowerCase();
console.log(name) // still capital-D "Dave"

name = name.toLowerCase()
console.log(name) // now it's "dave"

Every other type: Objects, Arrays, etc.

The other category is the object type. This encompasses objects, arrays, functions, and other data stuctures like Map and Set. They are all objects.

The big difference from primitive types is that objects are mutable! You can change the value in the box.

Mutable and immutable JavaScript types

Immutable is Predictable

If you pass a primitive value into a function, the original variable you passed in is guaranteed to be left alone. The function can’t modify what’s inside it. You can rest assured that the variable will always be the same after calling a function – any function.

But with objects and arrays (and the other object types), you don’t have that assurance. If you pass an object into a function, that function could change your object. If you pass an array, the function could add new items to it, or empty it out entirely.

So this is one reason why a lot of people in the JS community try to write code in an immutable way: it’s easier to figure out what the code does when you’re sure your variables won’t change unexpectedly. If every function is written to be immutable by convention, you never need to wonder what will happen.

A function that doesn’t change its arguments, or anything outside of itself, is called a pure function. If it needs to change something in one of its arguments, it’ll do that by returning a new value instead. This is more flexible, because it means the calling code gets to decide what to do with that new value.

Recap: Variables Point to Boxes, and Primitives are Immutable

We’ve talked about how assigning or reassigning a variable effectively “points it at a box” that contains a value. And how assigning a literal value (as opposed to a variable) creates a new box and points the variable at it.

let num = 42
let name = "Dave"
let yes = true
let no = false
let person = {
  firstName: "Dave",
  lastName: "Ceddia"
}
let numbers = [4, 8, 12, 37]

Variables point to boxes, boxes contain values

This is true for primitive and object types, and it’s true whether it’s the first assignment or a reassignment.

We’ve talked about how primitive types are immutable. You can’t change them, you can only reassign the variable to something else.

Now let’s look at what happens when you modify a property on an object.

Modifying the Contents of the Box

We’ll start with a book object representing a book in a library that can be checked out. It has a title and an author and an isCheckedOut flag.

let book = {
  title: "Tiny Habits",
  author: "BJ Fogg",
  isCheckedOut: false
}

Here’s our object and its values as boxes:

a Book object with 3 properties

And then let’s imagine we run this code:

book.isCheckedOut = true

Here’s what that does to the object:

Notice how the book variable never changes. It continues to point at the same box, holding the same object. It’s only one of that object’s properties that has changed.

Notice how this follows the same rules as earlier, too. The only difference is that the variables are now inside an object. Instead of a top-level isCheckedOut variable, we access it as book.isCheckedOut, but reassigning it works the exact same way.

The crucial thing to understand is that the object hasn’t changed. In fact, even if we made a “copy” of the book by saving it in another variable before modifying it, we still wouldn’t be making a new object.

let book = {
  title: "Tiny Habits",
  author: "BJ Fogg",
  isCheckedOut: false
}

let backup = book

book.isCheckedOut = true

console.log(backup === book)  // true!
console.log(backup.isCheckedOut)  // also true!!

The line let backup = book will point the backup variable at the existing book object. (it’s not actually a copy!)

Here’s how that would play out:

The console.log at the end further proves the point: book is still equal to backup, because they point at the same object, and because modifying a property on book didn’t change the shell of the object, it only changed the internals.

Variables always point to boxes, never to other variables. When we assign backup = book, JS immediately does the work to look up what book points to, and points backup to the same thing. It doesn’t point backup to book.

This is nice: it means that every variable is independent, and we don’t need to keep a sprawling map in our heads of which variables point to which other ones. That would be very hard to keep track of!

Mutating an Object in a Function

Wayyy back up in the intro I alluded to changing a variable inside a function, and how that sometimes “stays inside the function” and other times it leaks out into the calling code and beyond.

We already talked about how reassigning a variable inside a function will not leak out, as long as it’s a top-level variable like book or house and not a sub-property like book.isCheckedOut or house.address.city.

function doesNotLeak(word) {
  // this assignment does not leak out
  word = "world"
}

let test = "hello"
doesNotLeak(test)
console.log(test) // prints "hello"

After reassigning word

And anyway, this example used a string, so we couldn’t modify it even if we tried. (because strings are immutable, remember)

But what if we had a function that received an object as an argument? And then changed a property on it?

function checkoutBook(book) {
  // this change will leak out!
  book.isCheckedOut = true
}

let book = {
  title: "Tiny Habits",
  author: "BJ Fogg",
  isCheckedOut: false
}

checkoutBook(book);

Here’s what happens:

Look familiar? It’s the same animation from earlier, because the end result is exactly the same! It doesn’t matter whether book.isCheckedOut = true occurs inside a function or outside, because that assignment will modify the internals of the book object either way.

If you want to prevent that from happening, you need to make a copy, and then change the copy.

function pureCheckoutBook(book) {
  let copy = { ...book }

  // this change will only affect the copy
  copy.isCheckedOut = true

  // gotta return it, otherwise the change will be lost
  return copy
}

let book = {
  title: "Tiny Habits",
  author: "BJ Fogg",
  isCheckedOut: false
}

// This function returns a new book,
// instead of modifying the existing one,
// so replace `book` with the new checked-out one
book = pureCheckoutBook(book);

If you want to learn more about writing immutable functions like this, read my guide to immutability. It’s written with React and Redux in mind but most of the examples are plain JavaScript.

References in the Real World

With your newfound knowledge of references, let’s look at a few examples that could cause problems. See if you can spot the problem before reading the soltuion.

DOM Event Listeners

Quick background on how the event listener functions work: to add an event listener, call addEventListener with the event name and a function. To remove an event listener, call the removeEventListener with the same event name and the same function, as in the same function reference. (otherwise the browser can’t possibly know which function to remove, since an event can have multiple functions attached to it)

Have a look at this code. Is this using the add/remove functions correctly?

document.addEventListener('click', () => console.log('clicked'));
document.removeEventListener('click', () => console.log('clicked'));

Figured it out?

This code will never remove the event listener, because those two arrow functions are not referentially equal. They’re not the same function, even though they are identical as far as syntax goes.

Every time you write an arrow function () => { ... } or a regular function function whatever() { ... }, that creates a new object (functions are objects, remember).

Prove it! Try this in the console:

let a = () => {}
let b = () => {}
console.log(a === b)

It’ll print false! Every new object (array, function, Set, Map, etc.) lives in a brand new box, unequal to every other box.

To make the event listener example work correctly, store the function in a variable first, and pass that same variable to both add and remove.

const onClick = () => console.log('clicked');
document.addEventListener('click', onClick);
document.removeEventListener('click', onClick);

Unintended Mutation

Let’s look at another one. Here’s a function that finds the smallest item in an array by sorting it first, and taking the first item.

function minimum(array) {
  array.sort();
  return array[0]
}

const items = [7, 1, 9, 4];
const min = minimum(items);
console.log(min)
console.log(items)

What does this print?

If you said 1 and [7, 1, 9, 4], you’re only half right ;)

The .sort() method on arrays sorts the array in place, meaning it changes the order on the original array without copying it.

This example prints 1 and [1, 4, 7, 9].

Now, this might be what you wanted. But probably not, right? When you call a minimum function, you don’t expect it to rearrange the items in your array.

This kind of behavior can be especially confusing when the function lives in another file, or in a library, where the code isn’t right in front of you.

To fix this, make a copy of the array before sorting it, like in the code below. Here we’re using the spread operator to make a copy of the array (the [...array] part). This is actually creating a brand new array and then copying in every element from the old one.

function minimum(array) {
  const newArray = [...array].sort();
  return newArray[0]
}

Go Forth and Reference Well

This stuff comes up all the time, but it’s also one of those things you can kind of muddle through without knowing quite how it works.

It can take a little while to wrap your brain around the concept of “pointers”, variables pointing to values, and keeping references straight. If your brain feels like it’s in a fog right now, bookmark this article and come back in a week.

Once you get it, you’ve got it, and it’ll make all of your JS development go more smoothly.

This article is the first in a series on data structures and algorithms in JS. Next up is Linked lists in JavaScript! Now that you know how references work, linked lists will be way easier to understand.

I’m working on the next article in the series, on binary trees. Drop your email in the box if you want to be notified when it’s out!