Ladies and gentlemen, boys and girls, welcome to the Javascript magic show!
Today we will explore a few mysterious and tantalizing feats of magic. Hold on to your socks as we dive into the first trick.
Magic Trick #1
Take a look at this box, completely empty, no trapdoors, no hidden pouches, just a regular ‘ole Javascript object.
const box = {};
console.log(box);
// {}
Now if you take a look in our very stylish hat, it is also a regular run of the mill Javascript object.
const hat = box;
console.log(hat);
// {}
We’re going to place this bunny rabbit in the box, say a few magic words, and voila, the rabbit is going to teleport into the hat. Don’t worry folks, there’s breathing holes in the box. We’re not going to have an unconscious bunny on our hands.
box.rabbit = 'hi there';
“Abracadabra!”, I say as I wave my hands over the closed box.
What’s in the hat now? Any guesses?
console.log(hat);
// { rabbit: 'hi there' }
The rabbit is now in the hat! Truly incredible. How about we check what’s in the box?
console.log(box);
// { rabbit: 'hi there' }
Oh dear. This is a little disturbing. It seems that we’ve cloned the rabbit instead of teleporting it. Is it the exact same rabbit rabbit in the hat and box or is it two separate rabbits? We can check for equality to find out.
console.log(hat === box);
// true
* gasps in the audience. The same exact rabbit exists in two places at once! In fact, according to this equality check, the hat and the box are the same thing. What manner of sorcery is this? Let’s go further down the rabbit hole…
Magic Trick #2
For the next trick we’ll have several boxes within the box. This time, we’ll use Object.assign
to copy the entire object and use that structure for the hat as well. Both the hat and the box will have several boxes inside.
const box = { innerBox: { innerMostBox: {} } };
// we're creating a copy here, right? TOTAL SAFETY. Right? RIGHT?!?!?!?!
const hat = Object.assign({}, box); console.log(hat);
// { innerBox: { innerMostBox: { rabbit: 'hi there' } } }
Let’s put the rabbit in the box and take a look at what’s in the hat.
box.innerBox.innerMostBox.rabbit = 'hi there';
console.log(hat);
Since we had that misstep with magic trick #1, let’s see if we’ve done anything disturbing with the fabric of space and time.
console.log(hat === box);
// false
console.log(box.innerBox === hat.innerBox);
// true
console.log(box.innerBox.innerMostBox === hat.innerBox.innerMostBox);
// true
console.log(box.innerBox.innerMostBox.rabbit === hat.innerBox.innerMostBox.rabbit);
// true
Wow. Only slightly less disturbing. The hat and the box are no longer the same exact thing but not the inner boxes and the rabbits are referring to one thing in two places. Uhhh…let’s try something else here…
Magic Trick #3
I really liked the movie Inception. Let’s get a few volunteers from the audience and send them on a hypothetical mission. This trick will be a combination of the previous tricks. Don’t worry! They were outliers. There’s no way something disturbing could happen three times in a row.
First we’ll define their adventures.
const startHypotheticalAdventure = (team) => {
const alertTeam = awakenTeamInDreamWorld(team);
const resultsOfAdventure = sendTeamOnAdventure(alertTeam);
return resultsOfAdventure;
};
const sendTeamOnAdventure = (team) => {
// lucky team member is killed with a coffee mug
const luckyTeamMember = team.find((teamMember) => {
return teamMember.name === 'Bob';
});
luckyTeamMember.alive = false;
// luckier team member is beaten up by a bunch of school children
let luckierTeamMember;
team.forEach((teamMember) => {
if (teamMember.name === 'Rick') {
luckierTeamMember = teamMember;
}
});
luckierTeamMember.dignity = false;
// luckiest team member hides and loses self-respect
const luckiestTeamMember = team.find(teamMember => teamMember.name === 'Matt');
const hidingPlace = {
secretPassageway: {
secretRoom: {
trapDoor: {
closet: [luckiestTeamMember]
}
}
}
};
const cloneOfHidingPlace = Object.assign({}, hidingPlace);
cloneOfHidingPlace.secretPassageway.secretRoom.trapDoor.closet[0].selfRespect = false;
return [luckyTeamMember, luckierTeamMember, luckiestTeamMember];
};
const awakenTeamInDreamWorld = team => team.map(teamMember => {
teamMember.awakenedInDreamWorld = true;
return teamMember;
});
Then we’ll define our team, put them to sleep, and send them on this hypothetical adventure.
// we're hoping this will just happen in our imagination
const team = [
{ name: 'Bob', dignity: true, selfRespect: true },
{ name: 'Rick', dignity: true, selfRespect: true },
{ name: 'Matt', dignity: true, selfRespect: true }
];
const hypotheticalTeam = startHypotheticalAdventure(team);
// hypothetical team is a mess, no dignity, no self-respect,
// and not alive! Good thing this is a dream....
console.log(hypotheticalTeam);
// the team is still intact here. Right? RIGHT?!?!?
console.log(team);
OHHHH THE HUMANITY!!!! WHAT HAVE WE DONE!?!?! *shakes fist at the clouds
What was supposed to be a hypothetical exercise has turned into a real tragedy. Matt has no self-respect, Rick has no dignity, and Bob is dead.
Pulling back the curtain
Since you all look pretty frightened and disturbed, I’m going to break the cardinal rule of magic and explain how these tricks work.
These tricks work on the principle that Javascript arrays and objects are passed not as new values, but references to values. So when we assign hat
to the value of box
we are actually saying that those two variables will refer to the same place in memory. Same thing goes for arrays and objects we pass into functions of any sort, whether they are forEach
, map
, reduce
, or anything else.
For the first trick, when we assigned hat to the value of box, it was kind of like we showed the audience a box, put a sheet over it, then revealed the box again, only this time we’re calling it a hat. This is less disturbing because our concept of reality isn’t threatened, but rather, your concept of my mental stability.
The second trick employed Object.assign
, which contrary to popular belief, only performs a root level copy of the object. If there are nested objects and arrays, those will still refer to the same place in memory.
Finally, for the third trick, I wrote some awful code that represents the kinds of problems these Javascript phenomenon can cause in the wild. Imagine a more complex example where there are more variables involved. More mutations. You’d have quite a mess on your hands. In the unfortunate case of magic trick #3 we accidentally sent our team on a real adventure instead of a hypothetical one and Bob is dead.
Here are a few suggestions I have for avoiding accidental “magic” in the real world:
- Use
map
properly. Amap
is designed to return a new array but only if you return a new value in the iterator function and avoid mutations. If you are mutating things, or interacting with state outside the function scope, you should take a look at perhaps forEach or a for loop. - Don’t lie to me. When I see a function that returns a value and that value being assigned to a new var, my assumption is that the function isn’t changing the value being passed in as an argument. I’m expecting that a whole new value is being returned.
- If you are mutating the original reference, don’t make it seem like the function is creating a new value. Return nothing and perhaps add a comment. For example,
mutateOriginalArray(array)
instead ofconst newArray = mutateOriginalArray(array)
. - Be aware of the limitations of
Object.assign
as a way of copying objects. This only returns a new object at the root level. Perhaps this is ok. If you are hoping for a deep copy, it’ll be more work, because you’ll need to make copies of every nested objected and array as well. - Make copies of objects and arrays where practical – Consider using an immutability library. Other options include using
Array
methods that return a new array like.slice()
,.filter()
, and.map()
. The spread operator...
andObject.assign
are good options as well, keeping in mind their limitations.