Part 3: What is a Reference in JavaScript?
This is Part 3 of my I Need To Learn More JavaScript series.
- Part 1: What is a Closure in JavaScript?
- Part 2: What is forEach and Map in JavaScript?
- Part 3: What is a Reference in JavaScript?
- Part 4: What is Event Delegation in JavaScript?
- Part 5: What is Variable Scope in JavaScript?
What is a Reference?
When I was new to JavaScript, I recently was rushing out a feature on a side project of mine, Too Many Miles. I admit it — I didn’t test the new feature and introduced a lovely bug into my UI.
Let’s take a look at the code I wrote…
calculateMaxReward: function (rewards) {
let rewardList = rewards
rewardList.shift();
return _.maxBy(rewards, function (reward) {
return reward.amount;
}).amount;
}
The function above calculateMaxReward
returns the highest reward amount in an array after excluding the first reward amount in that array.
I didn’t want to mutate the original rewards array so I first decided (or so I thought) to make a copy of the rewards array
in a new variable called rewardList
.
let rewardList = rewards
Next I removed the first reward in the ‘newly copied array rewardList
’ with rewardList.shift()
. Now with the first value removed, I calculated and returned the largest reward amount.
Bugs? But why…
Interestingly the function above calculateMaxReward
worked liked I had planned. But now I was getting weird results when I graphed the original rewards array
.
As you can see above the last reward date graphed was for December 22nd, 2016. I knew the original rewards array contained a date from 2017. I even checked my database to confirm this! What was going on here? The January 2017 value was not showing up
The Reason
I modified the original array — rewards
.
When I wrote the following code, let rewardList = rewards
I thought I was making a copy of the original array rewards
. Instead I was making a reference to the original array rewards
.
calculateMaxReward: function (rewards) {
let rewardList = rewards //THIS DOES NOT COPY THE ARRAY
rewardList.shift();
return _.maxBy(rewards, function (reward) {
return reward.amount;
}).amount;
}
Strings, Numbers, and Booleans
Let’s take a look at the following example.
let age = 100;
let age2 = age;
console.log(age, age2) //prints out 100 100age = 200;
console.log(age, age2) //prints out 200 100
Strings, numbers, and booleans are primitive values in JavaScript. Changing the value of a variable never changes the underlying primitive value, it just points the variable to a new primitive.
When I wrote let age = 100
it sets the value 100
in memory and points the variable age
to the value in memory (100).
When I wrote let age2 = age
it copied the value in memory (100) and set it to a new value in memory (100). It then pointed the variable age2
to this new value in memory.
We now have 2 completely separate values in memory.
Finally when I wrote age = 200
it took the value in memory, 100
and reassigned it to a new value in memory 200
.
In the figure above, Box 1 and Box 2 under Primitives are values stored in memory. When I wrote let age2 = age
I literally copied over the memory value in box 1
and created a new memory value in box 2
.
We now have 2 completely separate values in memory. They just happen to have the same primitive value (number 100).
The takeaway is that numbers, strings, and booleans in JavaScript are stored by a value in memory.
However Objects and Arrays are stored by a reference.
…and this is where my bug comes back into play.
Objects and Arrays
Objects and arrays are stored by a reference. What does that mean?
Let’s say you have an array and you want to make a copy of it. So you might write the following code below.
const characters = [“Mickey”, “Minnie”, “Homer Simpson”]
let disneyCharacters = characters
console.log(disneyCharacters)//prints [“Mickey”, “Minnie”, “Homer Simpson”]
So far so good — this might actually work. Ok now let’s say that you wanted to modify the newly copied array disneyCharacters
because we ALL know that Homer Simpson is not a Disney character. So you might try something like this…
disneyCharacters[2] = “Donald Duck”
Now when we console.log the new array disneyCharacters
we get the following…
disneyCharacters//prints [“Mickey”, “Minnie”, “Donald Duck”]
Still working! But what happens when we view the original array characters
?
characters[“Mickey”, “Minnie”, “Donald Duck”]
What just happened?
I thought we made a copy of the array. Why did the original array change?
We created a reference to the original array and NOT a copy of the original array.
We updated disneyCharacters
, but disneyCharacters
is just a reference to the original array characters
.
Take a look at the Non-Primitives table below. When you write let age2 = age
you are NOT copying box 1 value into a new memory value. You are making a reference (box 2 arrow to box 1) the original object in box 1.
let disneyCharacters = characters
= is a reference to the original array.
Basically this reference is just a pointer that points the the original object which in our case is an array of characters. When we updated the reference (disneyCharacters) we update the original object (array characters).
To solve this we need to make a copy of the array.
How to Copy an Array in JavaScript?
I found 4 ways to do this. There are probably more. Yay JavaScript.
4 WAYS TO COPY A JAVASCRIPT ARRAYArray.prototype.slice()let disneyCharacters = characters.slice();Array.prototype.concat()let disneyCharacters = [].concat(characters);Array.from()let disneyCharacters = Array.from(characters);ES6 Spread Syntaxlet disneyCharacters = […characters];
What is a prototype? Read my article here.
Knowing what we know now, let’s copy the array correctly. Onward!
const characters = [“Mickey”, “Minnie”, “Homer Simpson”]let disneyCharacters = […characters] //using the ES6 spread syntax
disneyCharacters[2] = “Donald Duck” //updating new array
disneyCharacters
[“Mickey”, “Minnie”, “Donald Duck”] //new updated arraycharacters
[“Mickey”, “Minnie”, “Homer Simpson”] //doesn't affect original array
Now back to my original bug..
OLD CODE — I made a reference to the array
calculateMaxReward: function (rewards) {
let rewardList = rewards //This is the reference
rewardList.shift();
return _.maxBy(rewards, function (reward) {
return reward.amount;
}).amount;
}
NEW CODE — I make a copy of the array
calculateMaxReward: function (rewards) {
let rewardList = […rewards]; //This is the copy
rewardList.shift();
return _.maxBy(rewardList, function (reward) {
return reward.amount;
}).amount;
}
Bug fixed.
How to Copy an Object?
Like arrays, plain JavaScript objects must also be copied or the will succumb to the same problems with referencing.
Let’s say you have an object and try to copy it incorrectly.
const mouse = {
height: '4ft',
optimismLevel: 'high'
}const mickeyMouse = mouse;mickeyMouse.optimismLevel = "ecstatic"mickeyMouse
//prints Object {height: "4ft", optimismLevel: "ecstatic"}mouse
//prints Object {height: "4ft", optimismLevel: "ecstatic"}
The original mouse
object was updated. Just like arrays, when we wrote const mickeyMouse = mickey
we made a reference to the original object and not a copy of it.
To copy an object we use the Object.assign() method. Look at this example below:
const mickeyMouse = Object.assign({}, mouse , { optimismLevel: ‘ecstatic’ })mickeyMouse
Object {height: “4ft”, optimismLevel: “ecstatic”}mouse
Object {height: “4ft”, optimismLevel: “high”}
Object.assigns()’s first parameter is a new object you will be creating;{}
. The second parameter is the object you wish to copy which in our case is the mouse
object, and the third optional parameter are the object’s attributes you wish to modify when you copy the object.
For most cases this works wonderfully, but unfortunately Object.assign()
does not work in all cases. Object.assign
only copies one level deep, and if you have nested object data, it won’t copy it.
Here is an example of nested object data.
const mouse = {
height: ‘4ft’,
optimismLevel: ‘high’,
friends: {
friend1: ‘Goofy’,
friend2: ‘Minnie’
}
}const mickeyMouse = Object.assign({}, mouse) //copies mouse object
mickeyMouse.friends//prints Object {friend1: "Goofy", friend2: "Minnie"}mickeyMouse.friends.friend1 = "Pluto" //updates new object friend1
mickeyMouse.friends
//prints Object {friend1: "Pluto", friend2: "Minnie"}mouse.friends
// pritns Object {friend1: "Pluto", friend2: "Minnie"}
As you can see when I changed the new object mickeyMouse
friends it ALSO changed the original object mouse
despite using Object.assign()
!
How do you get around this?
Clone Deep
Typically you want to avoid this, but if you need to copy nested data, I would recommend using Lodash’s cloneDeep function which recursively runs their clone function. Yes you could write it yourself, but this will probably be most performant (but still not too performant).
const mouse = {
height: ‘4ft’,
optimismLevel: ‘high’,
friends: {
friend1: ‘Goofy’,
friend2: ‘Minnie’
}
}const mickeyMouse = _.cloneDeep(mouse)
mickeyMouse.friends
//prints Object {friend1: “Goofy”, friend2: “Minnie”}mickeyMouse.friends.friend1 = “Pluto”
mickeyMouse.friends
//prints Object {friend1: “Pluto”, friend2: “Minnie”}mouse.friends
//prints Object {friend1: “Goofy”, friend2: “Minnie”}
As you can see friend1
is now different between the two objects.
Well that is all folks! Hit me up if you have any questions! Keep Chugging People.