A few years back when I was studying Electronic Engineering, one of the requisites was Programming I, II, and III. C++ was the first programming language I learned, and I must say it was the reason I chose my current career (I realized I hated dealing with hardware). One of my professors was an old lady that used to teach in the classroom writing code on the board, instead of using a computer. Although I thought it was a waste of time, writing things down several times helped me understand the core concepts.
I remember a class when she explained references, and how every piece of data has a place in memory. At first, it was quite difficult to understand, but finally, she used the word "pointer". Ok, I know what a pointer is and I can picture it (kind of like an arrow, I will say), now what? Well, let's say we have a variable named myFavCar
that stores a value of "Ferrari"
. This piece of data is stored in someplace in the computer's memory, and it can be accessed by using its name, which is pointing to that place in memory. So, whenever we want to display the value of a variable, the function console.log
uses the variable name as an argument and gets the data from where is stored.
let myFavCar = "Ferrari"
console.log(myFavCar); // prints "Ferrari"
Primitives vs. Objects
If we want to reassign that value to "Lamborghini" instead, we should start talking about immutability, and how primitives (String
, Number
, Boolean
, Null
, and Undefined
) and immutable and how Objects
(pretty much everything else in JS) can be modified. Since we are using a String
variable, is not like Lamborghini will override Ferrari. We can't modify a String
, so what is going to happen is that myFavCar now will be pointing to another place in memory where this new value is stored.
let myFavCar = "Ferrari"
myFavCar = "Lamborghini";
console.log(myFavCar); // prints "Lamborghini"
This makes a lot of sense when I think about String methods returning a new String instead of modifying the current String value. That's why if we want to keep that new value, we have to store it in a new variable (aka, pointer to a new place in memory).
let text1 = "Hi";
let text2 = "there";
let text3 = text1.concat(" ", text2);
console.log(text3); // prints "Hi there"
// this does not modify text1 or text2 values
How does mutation work?
A mutation is something that we want to avoid at all costs, and that means making sure none of our functions change their arguments or anything outside of them. This kind of function is called pure function (I learned that today!). Whenever we pass an object as an argument for a function, that function can modify the object that's outside of the scope. A way to avoid this kind of mutation is to use the spread operator (...
) and save a copy of the object and modify and return that copy.
let customer = {
name: "John Doe",
dob: "2/13/1987",
address: "123 Park Ave",
subscriptionAct: true,
}
function updateInfo(customer) {
// create a variable that stores a copy of the object
// this will be a NEW object
let customerUpdated = { ...customer }
// modifies the copy
customerUpdated.subscriptionAct = false;
return customerUpdated;
}
// the function will return a new object
// we save it in a variable
customer = updateInfo(customer);
Without the spread operator, customerUpdated
will point to the customer object. So pretty much we will have customer and customerUpdated pointing at the same object. This is called copy by reference, and it means that if we modify either one, it will affect both of them.
Copy by value can be done with primitives, since they can't be modified, they just create a new place in memory to store that new value (even if it's a copy).
let a = "a";
let b = "b";
a = b;
b = "hello";
console.log(b); // prints "hello"
console.log(a); // prints "b"
In this case, a
copies the value that's stored in b
, which in this case is a String. After reassigning b
a new value, we can notice that a
didn't change. That's because it copied just the value, but does not reference that address where b
is pointing at.
Understanding these concepts was difficult and very confusing at the beginning, but after a few years of programming in different languages, I was able to understand, being a crucial part of data structures and algorithms. I used this article as a reference for writing this post, which helped me understand these concepts even more.
Top comments (2)
Some notes:
JavaScript uses primitives but their handling is made by their Object wrappers which have the "primitive's methods", see MDN Primitive.
The primitives are autoboxed/autowrapped as far I understand, so you never need to worry about it (unlike Java, C#, C++), just have in mind that they are like hybrid, i.e. they are primitives with Object methods, Objects that you could extend on the wrapper Object prototype (but not recommended to), but the values are still are immutable primitives as they may be "copied" when given to functions for example (actual "copy" may depend on the JS engine, see this StackOverflow question).
Yet, technically (due the lower level implementation, talking about memory usage), as far I understand and just for you to know, you can't do actual low level copy by value manually on JS as the primitives are wrapped on Object which variables reference to, so what you did on the
example is basically moving the refences from the variables. What I mean is that no extra memory should have been allocated on this example on JavaScript, but it would be on another language like C++, Java, C#.
Many people have already tried to explain topics related to this, specially on the function arguments, see this StackOverflow question "Is JavaScript a pass-by-reference or pass-by-value language?".
(Atlough this can become an issue as well if the JS Engine doesn't do the copy when you need it, for example splitting a very large string, saving the small result string into a variable, and discarding any reference to the original string.. Well Chrome/V8 used/still has the issue of referencing the very big string, see this StackOverflow question "How to force JavaScript to deep copy a string?")
On the other hand Java and C# (as far I remember), you can manually handle the primitives and the Object wrapped ones independently. See this article for Java.
This is more noticeable on C++, as you explained with more manual pointer handling and such (pointers exist on Java altough are automatic, and manual handling is possible on C# by the way, some even referenced C# as "C++++" for such similarity).
This topics are much more demostrable on the statically typed languages like C++, so bear in mind this when using and thinking with JavaScript.
Good luck on your learning!
Thank you!!