Variable assignment is a concept that many developers find confusing. In this post I will try to explain how JavaScript treats variable assignments and argument passing.
Key to this is understanding the difference between primitive values and objects. In JavaScript there are five types of primitive values - undefined, null, boolean, string and number. As you will see from my examples, javaScript treats primitive values differently from objects. The most important difference is that primitive values are manipulated by value and objects by reference.
What this really means is that primitive values will not be shared between multiple variables – even after setting variables equal to each other. Every variable representing a primitive value is guaranteed to represent a unique memory locations and no two variables will ever point to the same memory location. The other key point is that the value itself is stored in the physical memory location.
Object variables are different since multiple variables may point to a shared location in memory instead of representing multiple copies of the same data. Unlike primitive values, objects are not immutable, so you have to be careful when changing referenced data since the change will be seen by all references.
In the following code samples I will show some practical examples of this.
To better visualize the concepts I have included my examples as Jasmine unit tests.
Assigning primitive values:
First up is assignment of primitive values. Initially we assign the value 'Joe' to the variable joe. Next we create another variable, alsoJoe, and assign it to joe. Not surprisingly we see that both variables contain the same value. However, it's important to point out that this assignment does not tie joe and alsoJoe together. In fact all that happened was that the value from joe was copied into alsoJoe, so when we go to change alsoJoe we don't have to worry about affecting joe. This is because the two variables are backed by two distinct memory locations – with no crossover.
Assigning object values
The next example is very similar, but instead of primitive types we will be working with object references.
Initially this may not seem that different from the previous example, but the key difference is that assigning person1 to person2 will join the two object variables at the hip. Meaning you now have two handles to the same data and a change via one will affect the other. The only way to break the link is to reassign one of the variables to a different object. Reassigning will point the variable to a new memory location, but the other variable will be unaffected since it still points to its original location.
Passing arguments by value
Next up is passing primitive values to functions.
Based on our previous discussion it may not come as a total surprise that the change made to val inside incrementValue() is not seen outside the function. Instead of incrementing to 20 the value remains at 10. This is because the primitive value is passed as an independent copy of the original value and changes to the copy will not be reflected in the original value.
Passing objects (Call by Sharing)
Next we will look at how this changes if we instead pass an object reference to our function.
As you can see from the test, adding 10 to the original value inside incrementObjectValue() is visible outside the function. This is because objects are not passed as copies, but as memory references. Similar to our object assignment example, the argument of the function points to the same memory location as the original myObj variable.
The term for this behavior in JavaScript is call by sharing.
Call by sharing means that the arguments of the called function point to the same memory locations as the variables passed by the caller. However, we are still dealing with two sets of independent memory pointers.
As we have seen, mutating the reference inside the function will be seen by the caller. However, since it's an independent reference pointer, reassigning the reference inside the function will break the connection between the caller and function. Meaning if you assign the argument to a new object reference inside the function, you will not see that change from the caller's side.
This behavior is the same as the examples above where we reassigned variables that initially pointed to the same locations in memory.
The reassigning behavior is a key difference between call by sharing and call by reference. In languages where pass by reference is used, reassignment of references will be seen by the caller as well.
Lastly I have included an example with a subtle twist where I am passing myObj.val instead of the entire object. This may seem similar, but it's actually different since it's going back to passing a primitive value. As you can see, the effects are the same as in the previous example – the argument is passed by value and no changes are visible outside the function.