JavaScript: Pass-by-value and Pass-by-reference
Part of mastering a craft is understanding fundamental concepts
One of the first topics I came across when learning to program was pass-by-reference and pass-by-value. At the time I knew it was a fundamental concept, but thought I could get away by understanding it just well enough. Perhaps I did not quite understand why this concept is important.
Now after having to consult a hand full of style guidelines and language documentations I think I finally get it. In order to write readable and maintainable code, a software engineer should have a complete comprehension of what a function does to its caller. For example, a programmer should be able to break down how many responsibilities a function has and how many it should actually have and how this function affects the caller as well — whether the function mutates the argument being passed to it or whether it has any side-effects when returning a value.
These ordinary types of decisions in a life of a programmer — including being able to act on best practices such as this one, functions should have limited responsibility and should never mutate parameters — made more sense to me after I finally grasped the concept of a language behaving as pass-by-value or pass-by-reference. This idea is at the core of understanding how to invoke functions and what behaviour to expect.
Definition
In essence, when a parameter is passed-by-reference both variables — the variable we pass into the function as an argument — in the caller — and the local variable created with the same parameter’s name — in the callee — will point to the same address in computer memory.
As a result, all changes done by the callee will mutate the original object and all the variables that have reference to that object will see the change.
On line 1 of the example below, shoppingList
is initialised and assigned an array with four elements of type string. The function addOneKilo
on line 9 is called and passed shoppingList
as argument. Within the for
loop on lines 4–7 each element in the array is reassigned to a new string. Note that on line 11, the function console.log()
will print out shoppingList
with the new elements. We can conclude that addOneKilo
had a mutative effect on the original variable — the one we passed in as an argument to the function.
Note that reassignment in itself is a non-mutative action — each of the individual elements was simply assigned to a new value, but the result of this action did mutate the array. We can conclude that both shoppingList
and listItems
referenced the same address in memory, containing the array. Therefore, any modifications to this object will be visible to both variables.
When a parameter is passed-by-value, a copy of the contents of the actual value is passed to the callee. Caller and callee have two independent variables, each one pointing to a different space in memory. As a consequence, the changes made by the callee will not reflect on the caller or the original variable we passed as the argument.
What will the function console.log()
on lines 5 and 10 print out? Firstly let’s break down the problem. On line 1 we initialise shoppingItem
and assign to it the string “banana”. On line 8, we call the function exchange()
and pass shoppingItem
as argument. Then, the function is executed and the parameter item
is initialised and assigned to it a copy with the same value as its argumentshoppingItem
. They are two variables with the same value, but each pointing to a different memory address. On line 4, we reassign item
to a new value ‘pear’ and we can see that the function console.log()
prints out this new value. On line 10, the function console.log()
is called and passed the variable shoppingItem
as argument — since the execution of the previous function exchange
had no effect onshoppingItem
— “banana” is what is output. This is the behaviour we classify as ‘pass-by-value’.
What is the difference between the two examples?
Data types: immutable vs. mutable
JavaScript has two categories of data types: primitive types and objects. Primitive types are strings, numbers, bigint, boolean, undefined, symbols and null. Objects include simple objects (other languages refer to it as hash-maps), arrays (indexed lists), date object types and functions.
Let’s start with primitive data types since they are simpler to understand. Primitive data types are basic units of the language implementation. They are immutable data types since they are at the lowest level - atomic and indivisible. This means that:
the value of a primitive data type cannot be changed once it is set and assigned to a variable.
If a variable references a primitive type and you want to change its value, all you can do is reassign it to a new value. All operations on primitive values will evaluate to a new value. As stated in MDN JavaScript, it is important to notice that a variable can be reassigned to a new value, but the primitive data cannot be changed in the ways objects such as arrays and functions can.
On line 1, the variable qty
is initialised and assigned the number 5
— a primitive type. On line 3 the function double()
is defined and it should perform an operation on the primitive value that modifies it. On line 8, double()
is being called and passed in the primitive value as an argument. On line 9 the function console.log()
gets the original primitive value — the one assigned to qty
— and prints out its value. The operation on line 4 did not mutate the original value. What happens under the hood is that JavaScript takes a copy of the original primitive value passed in as an argument and creates a local copy.
Objects behave differently to primitive values
Objects are composed of primitive values or other objects. While these primitive values remain immutable, objects are mutable. We are able to change parts of the object and all the variables that have a reference to that object will see that change as well.
Our previous examples demonstrate that reassignment is not destructive, meaning that it does not change the original value of the variable. However, reassignment can be destructive depending on which data type (primitive vs. object) it is being applied to. As we saw earlier, reassigning a specific value in the array does not mutate the element but it does mutate the array itself. The elements of string type in the array shoppingList
are primitive data types and therefore, immutable. However, changing these elements to new values caused the array to be modified and mutated permanently.
Functions and Methods: mutable vs. immutable
There are basically two ways of mutating objects — by changing an element in an array or by performing operations that mutate it. Some functions and/or methods can mutate their arguments and have a destructive effect on the original object.
On line 3, the function console.log()
shows that the method.push()
mutated the original object referenced by theaddamsFamily
variable.
Let’s look at another example.
On line 3 of this last example, console.log()
shows that the method concat()
did not mutate the original object referenced by theaddamsFamily
variable.
We can correctly assume that not all functions and methods are destructive and in certain cases, such as in this last example objects can behave as pass-by-value and the caller is not altered by the callee — a function or method modifications are not visible to the caller.
In this case, how do we know if a method or function mutates the caller? Unfortunately, there is no way to guess which operations are mutative and which ones are not. The only way to know that is by consulting the language documentation and by testing in the terminal the effects of calling a function or method on a certain object.
In summary, primitive data types are immutable and consequently, always behave as pass-by-value. Objects act differently. They are mutable and can behave as either pass-by-value or pass-by-reference.
An object can behave as pass-by-value when we pass it as a parameter to a method or function and the modifications performed by these methods or functions do not affect the original object. We know that only a copy of the object is passed around. An object can also behave as pass-by-reference. This happens we pass it as a parameter to a method or function and the modifications performed by these methods or functions mutate the original object —the change is permanent and visible to all variables pointing to that object. In this case, a reference to the address in memory of the original object is passed around.
Conclusion
Programming is one of the most complex things I ever have endeavoured to learn. Writing the simplest program requires and a high level of abstraction and small parts unfold into more complex ideas in a spiral learning process. The mental models I have built when I started learning one year ago are still being unfolded in each step of the journey. Pass-by-reference and pass-by-value are one of these concepts. I had to revisit it a few times in order to appreciate its importance and understand its workings.
The concept of pass-by-value and pass-by-reference empowered me to understand how to write more maintainable code following best practices such as “functions should perform a particular task”. It helped me to see the effects of calling and using certain methods. Also, it gave me more control on what to expect when calling a certain function — understanding whether it will have any side-effects and whether it will modify the original value — and consequently, to make better decisions on how to build functions that are modular, objective and reusable.