Epic Web Conf late-bird tickets are available now, hurry!

Get your tickets here

Join the community and network with other great web devs.

Time's up. The sale is over

JavaScript Pass By Value Function Parameters

March 23rd, 2021 β€” 5 min read

by Jeffrey F Lin
by Jeffrey F Lin
No translations available.Add translation

Why doesn't this work?

function getLogger(arg) {
  function logger() {
    console.log(arg)
  }
  return logger
}

let fruit = 'raspberry'
const logFruit = getLogger(fruit)

logFruit() // "raspberry"
fruit = 'peach'
logFruit() // "raspberry" Wait what!? Why is this not "peach"?

So, to talk through what's happening here, I'm creating a variable called fruit and assigning it to a string 'raspberry', then I pass fruit to a function which creates and returns a function called logger which should log the fruit when called. When I call that function, I get a console.log output of 'raspberry' as expected.

But then I reassign fruit to 'peach' and call the logger again. But instead of getting a console.log of the new value of fruit, I get the old value of fruit!

I can side-step this by calling getLogger again to get a new logger:

const logFruit2 = getLogger(fruit)
logFruit2() // "peach" what a relief...

But why can't I just change the value of the variable and get the logger to log the latest value?

The answer is the fact that in JavaScript, when you call a function with arguments, the arguments you're passing are passed by value, not by reference. Let me briefly describe what's going on here:

function getLogger(arg) {
  function logger() {
    console.log(arg)
  }
  return logger
}

// side-note, this could be written like this too
// and it wouldn't make any difference whatsoever:
// const getLogger = arg => () => console.log(arg)
// I just decided to go more verbose to keep it simple

When getLogger is called, the logger function is created. It's a brand new function. When a brand new function is created, it looks around for all the variables it has access to and "closes over" them to form what's called a "closure". This means that so long as this logger function exists, it will have access to the variables in its parent's function and other module-level variables.

So what variables does logger have access to when it's created? Looking at the example again, it'll have access to fruit, getLogger, arg, and logger (itself). Read that list again, because it's critical to why the code works the way it does. Did you notice something? Both fruit and arg are listed, even though they're the exact same value!

Just because two variables are assigned the same value doesn't mean they are the same variable. Here's a simplified example of that concept:

let a = 1
let b = a

console.log(a, b) // 1, 1

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

Notice that even though we make b point to the value of variable a, we were able to change the variable a and the value b pointed to is unchanged. This is because we didn't point b to a per se. We pointed b to the value a was pointing to at the time!

I like to think of variables as little arrows that point to places in the computer's memory. So when we say let a = 1, we're saying: "Hey JavaScript engine, I want you to create a place in memory with the value of 1 and then create an arrow (variable) called a that points to that place in memory."

Then when we say: let b = a, we're saying "Hey JavaScript engine, I want you to create an arrow (variable) called b that points to the same place that a points to at the moment."

In the same way, when you call a function, the JavaScript engine creates a new variable for the function arguments. In our case, we called getLogger(fruit) and the JavaScript engine basically did this:

let arg = fruit

So then, when we later do fruit = 'peach', it has no impact on arg because they're completely different variables.

Whether you think of this as a limitation or a feature, the fact is that this is the way it works. If you want to keep two variables up-to-date with each other, there is a way to do that! Well, sorta. The idea is this: instead of changing where the arrows (variables) point, you can change what they're pointing to! For example:

let a = {current: 1}
let b = a

console.log(a.current, b.current) // 1, 1

a.current = 2
console.log(a.current, b.current) // 2, 2 πŸŽ‰

In this case, we're not reassigning a, but rather changing the value that a is pointing to. And because b happens to be pointed at the same thing, they both get the update.

So, let's apply this solution to our logger problem:

function getLatestLogger(argRef) {
  function logger() {
    console.log(argRef.current)
  }
  return logger
}

const fruitRef = {current: 'raspberry'}

const latestLogger = getLatestLogger(fruitRef)

latestLogger() // "raspberry"
fruitRef.current = 'peach'
latestLogger() // "peach" πŸŽ‰

The Ref suffix is short for "reference" which is to say that the value the variable points to is simply used to reference another value (which in our case is the current property of an object).

Conclusion

There are naturally trade-offs with this, but I'm glad that the JavaScript specification calls for function arguments to be passed by value rather than reference. And the workaround isn't too much trouble when you have the need (which is pretty rare because mutability makes programs harder to understand normally). Hope that helps! Good luck!

Kent C. Dodds
Written by Kent C. Dodds

Kent C. Dodds is a JavaScript software engineer and teacher. Kent's taught hundreds of thousands of people how to make the world a better place with quality software development tools and practices. He lives with his wife and four kids in Utah.

Learn more about Kent

If you found this article helpful.

You will love these ones as well.