A "Gotcha" of JavaScript's Pass-by-Reference

Adam Nathaniel Davis - Aug 24 '20 - - Dev Community

A few days ago, I posted an article talking about Pass By Reference Trolls. Specifically, I was calling out those people who stubbornly refuse to acknowledge JavaScript's native pass-by-reference behavior, despite any proof you might show them. (If you're so inclined, you can read the article here: https://dev.to/bytebodger/troll-hunting-101-javascript-passes-objects-by-reference-40c8)

Because I've grown so weary of the Pass By Reference Trolls, I put a note at the bottom of that article explaining that, in contrast to my normal behavior, I would not be replying directly to any comments. However, after reading a few replies, I realized that there is a critical area of ambiguity on this matter that probably fosters a good portion of the confusion.

Rather than reply to those commenters directly (which I swore I would not do), I realized that this might be a prime opportunity for a follow-on article. To be absolutely clear, the particular people who replied to my last article were not acting trollish in any way. They were presenting respectful and well-reasoned counter-points, which is why I thought it might be best just to clarify things in this article.

In fact, the more I thought about this over the weekend, the more I realized that this is kinda like the Monty Hall Problem. (If you're not well familiar with it, google it. No, seriously. Right now. Go google it. It's fascinating.)


Alt Text

JavaScript's Monty Hall Problem

I won't bore you with a thorough recap of the Monty Hall Problem. (But have I mentioned that, if you don't know about it, you should google it??)

The key thing that interests me about it is that, on one level, it's actually an extremely simple problem. There's no calculus. No advanced concepts of theoretical physics. No quantum mechanics. It's a very basic puzzle of elementary probabilities. And yet, if people haven't already been exposed to the problem, the vast majority will come to the absolutely wrong solution.

But it gets more interesting than that. There's something about the way that the problem is received in the brain that causes even advanced academics to become extremely defensive and/or combative about defending their erroneous solution.

Seriously. Learn about the Monty Hall Problem. Then find someone who's not familiar with it - preferably someone with advanced academic or professional status. Then, when they give you the wrong solution, watch as they protest, vehemently, about how mistaken they believe you are.

In this regard, pass-by-reference is strikingly similar to the Monty Hall Problem. Once someone gets it in their head that "JavaScript has no pass-by-reference!!!" it becomes nearly impossible to dislodge their erroneous conclusion.


Alt Text

The Setup

If you've been writing code for, oh... five minutes or so, nothing in this next example will surprise you. Nevertheless, it's important to illustrate the extremely simple concept at play:

// initialize our variables
let mostImportantNumber = 3.14;
let spanishNumbers = { one: 'uno', two: 'dos', three: 'tres' };

// use these variables to initialize some NEW variables
let answerToEverything = mostImportantNumber;
let germanNumbers = spanishNumbers;

// mutate the NEW variables to our liking
answerToEverything = 42;
germanNumbers.one = 'einz';
germanNumbers.two = 'zwei';
germanNumbers.three = 'drei';

// inspect the ORIGINAL variables
console.log(mostImportantNumber);  
// 3.14 - no surprise here
console.log(spanishNumbers); 
// {one: 'einz', two: 'zwei', three: 'drei'}
// wait a minute... that doesn't look like Spanish
Enter fullscreen mode Exit fullscreen mode

As I discussed in my previous article, there are some people who want to dive into pedantics with the argument that, "That's not passing by reference! Passing requires a function!"

Umm... no. It doesn't. You can "pass" a value into a new variable by using the value to initialize the variable. But even if we give in to the Passing Police, we can write this with a function and the effect is no different.

const mutate = (aNumber, numberNames) => {
  aNumber = 42;
  numberNames.one = 'einz';
  numberNames.two = 'zwei';
  numberNames.three = 'drei';
}

// initialize our variables
let mostImportantNumber = 3.14;
let spanishNumbers = { one: 'uno', two: 'dos', three: 'tres' };

// use these variables to initialize some NEW variables
let answerToEverything = mostImportantNumber;
let germanNumbers = spanishNumbers;

// mutate the NEW variables to our liking
mutate(mostImportantNumber, spanishNumbers);

// inspect the ORIGINAL variables
console.log(mostImportantNumber);  
// 3.14 - no surprise here
console.log(spanishNumbers); 
// {one: 'einz', two: 'zwei', three: 'drei'}
// wait a minute... that doesn't look like Spanish
Enter fullscreen mode Exit fullscreen mode

Strangely enough, I've never had anyone argue with me that the primitive value (mostImportantNumber) and the object (spanishNumbers) are treated the same. It's pretty clear to the naked eye that something different is happening with these variables. But I've still had multiple Language Nerds stubbornly tell me that both variables are passed by value - even though they are obviously processed at runtime in very different ways.

But as I mentioned above, there were two commenters on my previous article who gave interesting "gotcha" examples. And the more I thought about it, the more I became convinced that it may be examples like those that are causing some people confusion.

So let's explore the "counter examples" they gave...


Alt Text

Fumbled References

@iquardt gave this basic-yet-vexing example:

const foo = xs => {
  xs = [1];
};

let xs = [];
foo(xs);
console.log(xs);  // []
Enter fullscreen mode Exit fullscreen mode

On the surface, this seems to "break" my pass-by-reference position, right? I mean, the xs array is passed into foo(), where it's mutated. But the original xs array is unaltered. So... no pass-by-reference??

Well, let's dive a bit deeper.

First, the example is a bit convoluted because we have the same variable name outside and inside the function scope. This always makes it a bit harder to follow what's actually happening as we try to mentally organize the scopes. So I'll make it a little clearer by renaming the variable in the function signature.

const foo = someArray => {
  xs = [1];
};

let xs = [];
foo(xs);
console.log(xs);  // [1]
Enter fullscreen mode Exit fullscreen mode

This seems to "work" - but it has nothing to do with pass-by-value or pass-by-reference. It has to do with the fact that, inside the function, there is no definition for xs. But in JS, functions have access to variables in their calling scope. JS looks outside the function scope and finds a definition for xs and updates it accordingly.

To get a clearer view on pass-by-value/reference, let's complete the de-obfuscation of foo() by changing the name of the inner variable and also tweaking the outer one.

const foo = someArray => {
  someArray = [1];
};

let originalArray = [];
foo(originalArray);
console.log(originalArray);  // []
Enter fullscreen mode Exit fullscreen mode

This is functionally equivalent to @iquardt's first example. And just like in that example, the outer-scoped array of originalArray remains unchanged. This is where people claim that JS objects are not passed by reference. The thinking goes like this:

If objects (and arrays are objects) are passed-by-reference, then, someArray will be a reference to originalArray. And if that were true, then when we set someArray = [1], that change should be reflected in originalArray, outside the function scope.

But that's not what happens. So... pass-by-reference is false??

Umm... no.

The problem here is that people seem to have completely juggled the idea of an object with a reference to that object. They're similar - but they're not identical, interchangeable concepts.

A reference to an object allows us to perform operations on the original object. But here's the critical part: If we destroy that reference, we shouldn't be surprised when we can no longer perform operations on the original object.

That's exactly what's happening in this example. When foo() enters its instruction body, someArray is absolutely a reference to originalArray. But then, in the first line of the function's code, that reference is destroyed (overwritten) by an entirely different object: [1]

And we can illustrate this concept just by adding a few lines of code:

const foo = someArray => {
  someArray.push('pass');
  someArray.push('by');
  someArray = [1];
  someArray.push('reference');
};

let originalArray = [];
foo(originalArray);
console.log(originalArray);  // ['pass', 'by']
Enter fullscreen mode Exit fullscreen mode

When the function first starts running, someArray is 100% a reference to originalArray. That's why push() updates the contents of originalArray - because as long as we have that reference, we can perform operations on the original object.

But on the third line of the function, we do this: someArray = [1]; That code doesn't overwrite the entire value of originalArray. That line of code overwrites the reference which originally pointed to originalArray. Therefore, the first two push() statements are the only ones that are reflected back on the original array.

The key takeaway is this:

A reference allows you to manipulate some original object. But if you overwrite the reference, you haven't overwritten the original object, you've just overwritten the reference to that object.


This might be clearer if I add some comments to the code:

const foo = someArray => {
  // someArray is currently A REFERENCE to 
  // originalArray
  someArray.push('pass'); // this uses THE REFERENCE to
  // originalArray to add an item to the array
  someArray.push('by'); // this uses THE REFERENCE to
  // originalArray to add an item to the array
  someArray = [1]; // this OVERWRITES the reference -
  // someArray is now [1] - with NO RELATION to
  // originalArray 
  someArray.push('reference'); // this adds an item to
  // the new [1] array, which has no relation to
  // originalArray
};

let originalArray = [];
foo(originalArray);
console.log(originalArray);  // ['pass', 'by']
Enter fullscreen mode Exit fullscreen mode

In the interest of completeness, I'll also show @devdufutur's example:

function reassign(someStuff) {
  someStuff = { someInt: 42 };
}

let three = { someInt: 3 };
console.log("before reassign", three); // { someInt: 3 }
reassign(three);
console.log("after reassign", three); // { someInt: 3 }
Enter fullscreen mode Exit fullscreen mode

He used a traditional object, rather than an array, but the concept here is exactly the same. The original three object remains unchanged because, in the first line of his reassign() function, he overwrote (destroyed) the reference to three.

Notice that, in his example, he even used the term "reassign" - which is rather instructive. Because when he writes someStuff = { someInt: 3 };, that LoC isn't reassigning the original object. It's reassigning the someStuff variable from its reference to a brand new object, disconnected from the original object passed in the argument.

We can alter this example to highlight this same principle:

function reassign(someStuff) {
  someStuff.someInt = -1;
  someStuff.thisIsPassBy = 'reference';
  someStuff = { someInt: 42 };
  someStuff.lost = 'reference';
}

let three = { someInt: 3 };
console.log("before reassign", three); // { someInt: 3 }
reassign(three);
console.log("after reassign", three);  // { someInt: -1, thisIsPassBy: 'reference' }
Enter fullscreen mode Exit fullscreen mode

someInt is reassigned to -1. That works because it's an operation on the reference to three. We can even add new keys, because adding the key is also an operation on the same three object (using the still-functioning reference).

But our attempts to set someInt to 42 and lost to 'reference' are not reflected on the original three object. They can't be. Because, when we tried to set someInt to 42, we overwrote the reference to three.

Again, this might be a little clearer with some comments:

function reassign(someStuff) {
  // someStuff is currently A REFERENCE to 'three'
  someStuff.someInt = -1; // this uses THE REFERENCE to
  // 'three' to update the value of someInt
  someStuff.thisIsPassBy = 'reference'; // this uses THE
  // REFERENCE to 'three' to add the thisIsPassBy key
  someStuff = { someInt: 42 }; // this OVERWRITES the
  // reference - someStuff is now { someInt: 42 } - with
  // NO RELATION to 'three'
  someStuff.lost = 'reference'; // this adds the 'lost'
  // key to the new { someInt: 42 } object, which has no
  // relation to 'three'
}

let three = { someInt: 3 };
console.log("before reassign", three); // { someInt: 3 }
reassign(three);
console.log("after reassign", three);  // { someInt: -1, thisIsPassBy: 'reference' }
Enter fullscreen mode Exit fullscreen mode



Alt Text

Buckets

FWIW, I do understand that this is why some people (angrily) cling to the ideas that "Everything is passed by value!" and "Objects are passed by a value - that holds a reference." And if that nomenclature helps you understand what's going on, then great!

But when you look at the first example in this article and you stubbornly stamp your feet and declare, "They're both passed by value!" you're taking two examples, that clearly and demonstrably behave quite differently, and you're trying to wash away that difference with some unhelpful bromide.

I'm not asking anyone to think anything like me, but it helps me to think of variables not so much as "values" (which is already abstract as hell) or "references". Instead, I just think of them as... buckets.

Once you've accounted for space constraints, you can put pretty much anything you want inside the bucket. It can hold a quantifiable volume (number) or some printed text (a string) or nothing (null) or... many other things.

Buckets can also hold objects. But when we try to pass the bucket to someone else, the recipient receives a note that says, "Hey, the original thing you're looking for is over there." If you take the note out of the bucket and replace it with something else entirely (even if that "something else" is... another object), there's no way for the original object to reflect any of those changes.

A bucket (variable) can hold some thing (like, a value) or it can hold a note that refers to something else. If the bucket is holding a reference, and you overwrite that reference with an assignment operator, you will wipe out the reference. And you won't see any future changes reflected on the original object.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player