Illustration by

Why Mutation Can Be Scary

A note from the editors: This article contain sample lessons from Learn JavaScript, a course that helps you learn JavaScript to build real-world components from scratch.

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?

Article Continues Below

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.

About the Author

Zell Liew

Zell is a freelance developer from Singapore. When he’s not working on client projects, he digs deep into code and explains what he learned on his blog. He has written books and courses, which include Automate Your Workflow and Learn JavaScript.

4 Reader Comments

  1. 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

  2. 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?

Got something to say?

We have turned off comments, but you can see what folks had to say before we did so.

More from ALA

I am a creative.

A List Apart founder and web design OG Zeldman ponders the moments of inspiration, the hours of plodding, and the ultimate mystery at the heart of a creative career.
Career