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.
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,
shoppingListis 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
shoppingListwith 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
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
shoppingItemand 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 argument
shoppingItem . 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
exchangehad no effect on
shoppingItem — “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
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.
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
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 the
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 the
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.
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.