To mutate means to change in form or nature. Something that’s mutable can be changed, while something that’s immutable cannot be changed. To understand mutation, think of the X-Men. In X-Men, people can suddenly gain powers. The problem is, you don’t know when these powers will emerge. Imagine your friend turns blue and grows fur all of a sudden; that’d be scary, wouldn’t it?
In JavaScript, the same problem with mutation applies. If your code is mutable, you might change (and break) something without knowing.
Objects are mutable in JavaScript#section2
In JavaScript, you can add properties to an object. When you do so after instantiating it, the object is changed permanently. It mutates, like how an X-Men member mutates when they gain powers.
In the example below, the variable egg
mutates once you add the isBroken
property to it. We say that objects (like egg
) are mutable (have the ability to mutate).
const egg = { name: "Humpty Dumpty" };
egg.isBroken = false;
console.log(egg);
// {
// name: "Humpty Dumpty",
// isBroken: false
// }
Mutation is pretty normal in JavaScript. You use it all the time.
Here’s when mutation becomes scary.
Let’s say you create a constant variable called newEgg
and assign egg
to it. Then you want to change the name of newEgg
to something else.
const egg = { name: "Humpty Dumpty" };
const newEgg = egg;
newEgg.name = "Errr ... Not Humpty Dumpty";
When you change (mutate) newEgg
, did you know egg
gets mutated automatically?
console.log(egg);
// {
// name: "Errr ... Not Humpty Dumpty"
// }
The example above illustrates why mutation can be scary—when you change one piece of your code, another piece can change somewhere else without your knowing. As a result, you’ll get bugs that are hard to track and fix.
This weird behavior happens because objects are passed by reference in JavaScript.
Objects are passed by reference in JavaScript#section3
To understand what “passed by reference” means, first you have to understand that each object has a unique identity in JavaScript. When you assign an object to a variable, you link the variable to the identity of the object (that is, you pass it by reference) rather than assigning the variable the object’s value directly. This is why when you compare two different objects, you get false
even if the objects have the same value.
console.log({} === {}); // false
When you assign egg
to newEgg
, newEgg
points to the same object as egg
. Since egg
and newEgg
are the same thing, when you change newEgg
, egg
gets changed automatically.
console.log(egg === newEgg); // true
Unfortunately, you don’t want egg
to change along with newEgg
most of the time, since it causes your code to break when you least expect it. So how do you prevent objects from mutating? Before you understand how to prevent objects from mutating, you need to know what’s immutable in JavaScript.
Primitives are immutable in JavaScript#section4
In JavaScript, primitives (String, Number, Boolean, Null, Undefined, and Symbol) are immutable; you cannot change the structure (add properties or methods) of a primitive. Nothing will happen even if you try to add properties to a primitive.
const egg = "Humpty Dumpty";
egg.isBroken = false;
console.log(egg); // Humpty Dumpty
console.log(egg.isBroken); // undefined
const
doesn’t grant immutability#section5
Many people think that variables declared with const
are immutable. That’s an incorrect assumption.
Declaring a variable with const
doesn’t make it immutable, it prevents you from assigning another value to it.
const myName = "Zell";
myName = "Triceratops";
// ERROR
When you declare an object with const
, you’re still allowed to mutate the object. In the egg
example above, even though egg
is created with const
, const
doesn’t prevent egg
from mutating.
const egg = { name: "Humpty Dumpty" };
egg.isBroken = false;
console.log(egg);
// {
// name: "Humpty Dumpty",
// isBroken: false
// }
Preventing objects from mutating#section6
You can use Object.assign
and assignment to prevent objects from mutating.
Object.assign
#section7
Object.assign
lets you combine two (or more) objects together into a single one. It has the following syntax:
const newObject = Object.assign(object1, object2, object3, object4);
newObject
will contain properties from all of the objects you’ve passed into Object.assign
.
const papayaBlender = { canBlendPapaya: true };
const mangoBlender = { canBlendMango: true };
const fruitBlender = Object.assign(papayaBlender, mangoBlender);
console.log(fruitBlender);
// {
// canBlendPapaya: true,
// canBlendMango: true
// }
If two conflicting properties are found, the property in a later object overwrites the property in an earlier object (in the Object.assign
parameters).
const smallCupWithEar = {
volume: 300,
hasEar: true
};
const largeCup = { volume: 500 };
// In this case, volume gets overwritten from 300 to 500
const myIdealCup = Object.assign(smallCupWithEar, largeCup);
console.log(myIdealCup);
// {
// volume: 500,
// hasEar: true
// }
But beware! When you combine two objects with Object.assign
, the first object gets mutated. Other objects don’t get mutated.
console.log(smallCupWithEar);
// {
// volume: 500,
// hasEar: true
// }
console.log(largeCup);
// {
// volume: 500
// }
Solving the Object.assign
mutation problem#section8
You can pass a new object as your first object to prevent existing objects from mutating. You’ll still mutate the first object though (the empty object), but that’s OK since this mutation doesn’t affect anything else.
const smallCupWithEar = {
volume: 300,
hasEar: true
};
const largeCup = {
volume: 500
};
// Using a new object as the first argument
const myIdealCup = Object.assign({}, smallCupWithEar, largeCup);
You can mutate your new object however you want from this point. It doesn’t affect any of your previous objects.
myIdealCup.picture = "Mickey Mouse";
console.log(myIdealCup);
// {
// volume: 500,
// hasEar: true,
// picture: "Mickey Mouse"
// }
// smallCupWithEar doesn't get mutated
console.log(smallCupWithEar); // { volume: 300, hasEar: true }
// largeCup doesn't get mutated
console.log(largeCup); // { volume: 500 }
But Object.assign
copies references to objects#section9
The problem with Object.assign
is that it performs a shallow merge—it copies properties directly from one object to another. When it does so, it also copies references to any objects.
Let’s explain this statement with an example.
Suppose you buy a new sound system. The system allows you to declare whether the power is turned on. It also lets you set the volume, the amount of bass, and other options.
const defaultSettings = {
power: true,
soundSettings: {
volume: 50,
bass: 20,
// other options
}
};
Some of your friends love loud music, so you decide to create a preset that’s guaranteed to wake your neighbors when they’re asleep.
const loudPreset = {
soundSettings: {
volume: 100
}
};
Then you invite your friends over for a party. To preserve your existing presets, you attempt to combine your loud preset with the default one.
const partyPreset = Object.assign({}, defaultSettings, loudPreset);
But partyPreset
sounds weird. The volume is loud enough, but the bass is non-existent. When you inspect partyPreset
, you’re surprised to find that there’s no bass in it!
console.log(partyPreset);
// {
// power: true,
// soundSettings: {
// volume: 100
// }
// }
This happens because JavaScript copies over the reference to the soundSettings
object. Since both defaultSettings
and loudPreset
have a soundSettings
object, the one that comes later gets copied into the new object.
If you change partyPreset
, loudPreset
will mutate accordingly—evidence that the reference to soundSettings
gets copied over.
partyPreset.soundSettings.bass = 50;
console.log(loudPreset);
// {
// soundSettings: {
// volume: 100,
// bass: 50
// }
// }
Since Object.assign
performs a shallow merge, you need to use another method to merge objects that contain nested properties (that is, objects within objects).
Enter assignment.
assignment#section10
assignment is a small library made by Nicolás Bevacqua from Pony Foo, which is a great source for JavaScript knowledge. It helps you perform a deep merge without having to worry about mutation. Aside from the method name, the syntax is the same as Object.assign
.
// Perform a deep merge with assignment
const partyPreset = assignment({}, defaultSettings, loudPreset);
console.log(partyPreset);
// {
// power: true,
// soundSettings: {
// volume: 100,
// bass: 20
// }
// }
assignment copies over values of all nested objects, which prevents your existing objects from getting mutated.
If you try to change any property in partyPreset.soundSettings
now, you’ll see that loudPreset
remains as it was.
partyPreset.soundSettings.bass = 50;
// loudPreset doesn't get mutated
console.log(loudPreset);
// {
// soundSettings {
// volume: 100
// }
// }
assignment is just one of many libraries that help you perform a deep merge. Other libraries, including lodash.merge and merge-options, can help you do it, too. Feel free to choose from any of these libraries.
Should you always use assignment over Object.assign
?#section11
As long as you know how to prevent your objects from mutating, you can use Object.assign
. There’s no harm in using it as long as you know how to use it properly.
However, if you need to assign objects with nested properties, always prefer a deep merge over Object.assign
.
Ensuring objects don’t mutate#section12
Although the methods I mentioned can help you prevent objects from mutating, they don’t guarantee that objects don’t mutate. If you made a mistake and used Object.assign
for a nested object, you’ll be in for deep trouble later on.
To safeguard yourself, you might want to guarantee that objects don’t mutate at all. To do so, you can use libraries like ImmutableJS. This library throws an error whenever you attempt to mutate an object.
Alternatively, you can use Object.freeze
and deep-freeze. These two methods fail silently (they don’t throw errors, but they also don’t mutate the objects).
Object.freeze
and deep-freeze#section13
Object.freeze
prevents direct properties of an object from changing.
const egg = {
name: "Humpty Dumpty",
isBroken: false
};
// Freezes the egg
Object.freeze(egg);
// Attempting to change properties will silently fail
egg.isBroken = true;
console.log(egg); // { name: "Humpty Dumpty", isBroken: false }
But it doesn’t help when you mutate a deeper property like defaultSettings.soundSettings.base
.
const defaultSettings = {
power: true,
soundSettings: {
volume: 50,
bass: 20
}
};
Object.freeze(defaultSettings);
defaultSettings.soundSettings.bass = 100;
// soundSettings gets mutated nevertheless
console.log(defaultSettings);
// {
// power: true,
// soundSettings: {
// volume: 50,
// bass: 100
// }
// }
To prevent a deep mutation, you can use a library called deep-freeze, which recursively calls Object.freeze
on all objects.
const defaultSettings = {
power: true,
soundSettings: {
volume: 50,
bass: 20
}
};
// Performing a deep freeze (after including deep-freeze in your code per instructions on npm)
deepFreeze(defaultSettings);
// Attempting to change deep properties will fail silently
defaultSettings.soundSettings.bass = 100;
// soundSettings doesn't get mutated anymore
console.log(defaultSettings);
// {
// power: true,
// soundSettings: {
// volume: 50,
// bass: 20
// }
// }
Don’t confuse reassignment with mutation#section14
When you reassign a variable, you change what it points to. In the following example, a
is changed from 11
to 100
.
let a = 11;
a = 100;
When you mutate an object, it gets changed. The reference to the object stays the same.
const egg = { name: "Humpty Dumpty" };
egg.isBroken = false;
Wrapping up#section15
Mutation is scary because it can cause your code to break without your knowing about it. Even if you suspect the cause of breakage is a mutation, it can be hard for you to pinpoint the code that created the mutation. So the best way to prevent code from breaking unknowingly is to make sure your objects don’t mutate from the get-go.
To prevent objects from mutating, you can use libraries like ImmutableJS and Mori.js, or use Object.assign
and Object.freeze
.
Take note that Object.assign
and Object.freeze
can only prevent direct properties from mutating. If you need to prevent multiple layers of objects from mutating, you’ll need libraries like assignment and deep-freeze.
just started learning advanced javascript. thanks for the great article.
Zell liew, you are a really smart guy, that’s a really long detailed article, we plan on using advanced javascript on one of our project app2c & your article is going to come in handy, Thanks
Hi Zell, I enjoyed the article. You mention lodash.assign as a way to do a deep merge. lodash.assign does a shallow merge, like Object.assign. Maybe you were thinking of lodash.merge?
Very Useful article! Keep it up. SBI Clerk Recruitment