Passing a Variable by Value or Reference

I'm a little later than usual for this week's blog post, so my apologies. I was a little under the weather and working on a conference talk proposal so I wasn't able to write about or research this topic earlier in the week. But this week's post is a good one.

Passing a Variable by Value or Reference is a topic that comes up as a common interview question. From my initial research into the differences between passing a variable by value and passing a variable by reference, it seemed to be a simple matter of understanding how JavaScript handles memory when data gets copied, either by setting variables equal to one another, or passing variables as arguments within function calls. It also seemed like there was a clear demarcation: if the data being passed was a Primitive Type (i.e. undefined, Boolean, Number, String, BigInt, Symbol, or null), then it would be passed by value, and if the data was a Structural Type (i.e. an Object or a Function), then it would be passed by reference.

However, the more I dug into the topic, the more confusion and inconsistencies I found across various blog posts and Stack Overflow threads. This blog post is an attempt to set the record straight.

In JavaScript variables are always passed by value. Full stop. Well, sort of a stop. When that variable is an Object or a Function, the value that gets passed is a reference to that Object or Function. This latter point gives rise to some behavior that can be confusing and seem inconsistent with other data types if not properly understood.

So let's break it down. We'll start by illustrating what it means to pass a variable by value.

Passing a Variable by Value

Consider the following code:

var a = 100;
var b = a;

console.log(a); // 100
console.log(b); // 100

This code is pretty straight-forward. We are first creating a variable named a, and assigning it the value of 100, which is a Number. Then we are creating a variable named b, and assigning its value as the value of a. When we console.log(a), we get 100. When we console.log(b), we get 100.

But what happens when we do the following:

var a = 100;
var b = a;

a = 200;

console.log(a); // 200
console.log(b); // 100

In this code we see some interesting and perhaps unexpected behavior. We are still creating the variable a, then assigning it the value of 100, which is still a Number. Then we are creating the variable b and assigning its value as the value of a. However, now we reassign a to the value of 200. When we console.log(a), we get 200. When we console.log(b), we get...100. What? a's former value? I thought b was equal to a?

This behavior happens because of the way that passing variables works with memory in JavaScript.

When we create the variable a and assign it the value of 100, we are allocating a location (or address) in memory to hold that value, let's call that location 0x001. The variable a points to that memory location.

When we create the new variable b, and assign it the value of the first variable (a), the assignment operation allocates a new location in memory, separate from the location that a points to. Let's call this new location 0x002. It then passes the value of the data held at 0x001 to 0x002, and points the variable b to 0x002.

It's crucial to understand that when we copy that data, even though both variables are seemingly based off of the same value, we are passing the value from one memory location to another, separate memory location. Because of this, even when we reassign the value of a to 200, we are in effect changing the data stored at location 0x001 to be the Number 200, but the value at location 0x002 is still 100. That is why we get the behavior in the code above.

Passing a Variable by Value: Function Calls

Another, more practical area where passing by value comes into play is with arguments passed to functions. Consider the following code:

function doubleValue(num) {
  num = num + num;
  return num;
}

var val = 100;
var result = doubleValue(val);

console.log(result); // 200
console.log(val); // 100

The code above demonstrates the same concept, just in a different context. We define a function named doubleValue, which accepts a parameter named num. Within the function block, we add num to itself, effectively mutating num to be double its initial value. We then return num.

Below that we create a variable named val, and assign it the value of 100. We then create a variable named result, and set its value equal to an invocation of the doubleValue function, with val passed as its argument.

When we do this, we are passing the variable by value. The function argument creates a new memory location with the same primitive value of 100, and any work done with this value is done separately from the original value and memory space of the val variable. Because of this, when we console.log(result), we get 200, and when we console.log(val), it is still 100.

Objects and Functions

Now consider the following code:

var person1 = {
  name: "Joey",
}

var person2 = person1;

console.log(person1.name); // "Joey"
console.log(person2.name); // "Joey"

Once again this is pretty straight-forward. We define a variable named person1, and assign it the value of an Object. Within this Object there is only one property, name, whose value is the String "Joey". We then create a second variable named person2, and assign its value to be person1.

When we console.log(person1.name) and console.log(person2.name) we get the same value in both instances: the String "Joey".

But now let's see what happens with the following change:

var person1 = {
  name: "Joey",
}

var person2 = person1;

person2.name = "Joseph";

console.log(person1.name); // "Joseph"
console.log(person2.name); // "Joseph"

Whoa! When we reassign the name property within the person2 Object, the name property within person1 also changes!

It would seem that, unlike with Primitive Data Types, when we work with Objects (and thereby Functions), new memory is not allocated when a second variable is assigned the value of the original variable's Object value. By all appearances, it would seem they both point to the same location in memory that person1 points to, so when a property is changed in person2, it is also changed in person1 and vice-versa.

Passing a Variable by Reference: Function Calls

We observe the same data mutation when we pass an Object as an argument to a function call. Consider the following code:

function changeName(obj) {
  obj.name = "Joseph";
  return obj;
}

var person = {
  name: "Joey",
}

var result = changeName(person);
console.log(result.name); // "Joseph"
console.log(person.name); // "Joseph"

So here we have defined a function named changeName that accepts a parameter named obj. Within the function block we are changing the value of the name property within obj to be the String "Joseph" and then returning obj.

Below that we define a variable named person, which is an Object with one property, name, with the value of the String "Joey". Then we create a variable named result, and its value is assigned the result of calling the changeName function with person passed as the argument. After calling this, when we console.log(result.name) and console.log(person.name), the output is the same: "Joseph".

Okay that aligns with the behavior observed in our previous example. The name property within the Object is mutated both in the original Object and the one the function call creates. What we're seeing here is often referred to as Pass by Reference behavior. A lot of blog posts state that when creating new copies of Objects, we are copying a reference to that data, so the memory location is the same for both variables, and any changes to one are reflected in the other.

That would seem to make sense, but what happens if we try to reassign the entire Object itself? If it's Pass by Reference then the reassignment should carry through for both variables, right?

function changeName(obj) {
  obj = {
    name: "Joseph",
  }

  return obj;
}

var person = {
  name: "Joey",
}

var result = changeName(person);
console.log(result.name); // Joseph
console.log(person.name); // Joey

In this code we've adjusted the changeName function to create an entirely new Object, which contains the property-value pair name: "Joseph", rather than just changing the name property directly on the obj Object. After calling this function and passing it the same person Object we've been working with, we observe that we actually now have two Objects with different values for their respective name properties. result.name returns "Joseph", but person.name returns the original value of "Joey".

What the heck?!? Oh no. Well let's go back to our previous example and try to reassign person2 to a different Object.

var person1 = {
  name: "Joey",
};

var person2 = person1;

person2 = {
  name: "Joseph",
}

console.log(person1.name); // "Joey"
console.log(person2.name); // "Joseph"

Oh nooooo! What the heck is going on here? Isn't this just passing by value behavior?

The answer is yes. There is a lot of confusion around this topic, and a lot of blog posts (including some of those referenced as sources below) get it wrong when talking about Objects and Functions, stating that they are passed by reference. However, when we change the value of a second Object whose value was previously set to equal the first Object, only the second Object's value changes. This is identical to the behavior of Primitive data types. And in fact, it is passing by value when working with Objects -- but that value is itself a reference.

My head hurts. This is really confusing, but the reality is that what a lot of folks commonly refer to as "Passing by Reference" is in fact Call by Sharing.

What this means in practice is that when you change the value of the second Object, that won't affect the original Object. However, when you change the internals of the second Object (i.e., its properties), that change does propagate up to the original Object. That happens because the value being passed is a reference.

Kudos to discussions within this Stack Overflow thread for pointing out this really important (but confusing) distinction.

Update: ES6 Modules

ES6 introduces first class support for modules, which is a way to organize functionality in your code. You can bundle common functionality into a module file, export the data or functionality you want from that module, and then import that exported data or functionality elsewhere in your program.

Say you have a function greeting() in a greeting.js file that is located within a modules directory. If you want to use that greeting() functionality elsewhere, you can now export it:

// modules/greeting.js:
export function greeting(name) {
  console.log(`Howdy ${name}`);
}

By using the export keyword, you are making that function available to be imported in other files in your program. For instance, in a top-level index.js file:

// index.js:
import { greeting } from "./modules/greeting.js";

greeting("Joey");
// expected result: "Howdy Joey"

For folks who have worked with JavaScript prior to ES6, this is a pretty radical change in how we can write and share functionality. This post will not get into all of the details of modules (that could very well be its own post or series of posts), but there are implications with regards to passing variables by value or reference. Consider the following:

// modules/coolNumbers.js:
var coolNumber = 66;

export { coolNumber };

We have created a new module named coolNumbers. In it we have a variable, coolNumber, with the value of 66 that we are exporting.

Back in index.js, let's import coolNumber:

// index.js:
import { coolNumber } from "./modules/coolNumbers.js";

console.log(coolNumber);
// expected result: 66

Great, so we can import our coolNumber and present its value. What's the big deal? Well let's update that value later on in modules/coolNumbers.js:

// modules/coolNumbers.js:
var coolNumber = 66;

export { coolNumber };

// later
coolNumber = 420;

Now if we access coolNumber in index.js, will it be the 66 value we initially export, or the 420 value that we reassign later on?

// index.js:
import { coolNumber } from "./modules/coolNumbers.js";

console.log(coolNumber);
// expected result: 420

We got 420, even though we initially exported 66. So what's happening here? When we import the coolNumber binding, regardless of whether it happens before or after the coolNumber = 420 assignment, that binding always resolves to 420, not the 66 value. That's because the value is passed by reference. This is now the only way that values are passed by reference in JavaScript, and it was introduced by ES6 modules.

Summary

When talking about passing by value vs. passing by reference, this topic refers to how memory works with certain data types between two variables, one assigned the value of the other, or when a variable gets passed to a function call.

Passing by value is clearly and easily demonstrated among the Primitive data types:

  • undefined
  • Boolean
  • Number
  • String
  • BigInt
  • Symbol
  • null

However, Passing by Reference is commonly confused for Call by Sharing behavior, which is observed with Structural data types:

  • Object
  • Function

When changing the values of properties within an Object or Function, those property values change for both the original Object and its copy. Same with Functions. However, changing the actual value of either Object (not just internal properties) ultimately shows that the two Objects' values do indeed occupy different memory locations, and what actually happens is passing by value, but the value is itself a reference, and adjustments to the properties within either Object or Function propagates up to the original Object or Function. This behavior creates a lot of confusion around this topic, but what is actually being observed is called Call by Sharing.

There is no true Passing by Reference in JavaScript. Everything is Passing by Value. However, when what is being passed is an Object or a Function, the value that gets passed is a reference.

Passing by Reference only occurs through ES6 Modules in JavaScript. For all other situations, everything is Passing by Value. However, when what is being passed is an Object or a Function, the value that gets passed is a reference.

Sources / Further Reading

These are articles that get Passing by Value right, but mislead on Passing by Reference:

These Stack Overflow threads are good discussions on the common confusion around Passing By Reference and what Call by Sharing is:

Kyle Simpson outlines how ES6 Modules use Passing By Reference in Chapter 3: Organization of You Don't Know JS: ES6 & Beyond: