📜 ⬆️ ⬇️

Javascript and horror mutations

Mutation is a change. Change the shape or change the essence. What is subject to mutations may vary. In order to better understand the nature of the mutation - think about the heroes of the film "X-Men". They could suddenly get tremendous opportunities. However, the problem is that it is not known exactly when these opportunities will manifest. Imagine that your comrade for no apparent reason turned blue and overgrown with wool. Scary, right? There are the same problems in JavaScript. If your code is subject to mutations, it means that you can, quite unexpectedly, change something and break it.



Objects in javascript and mutation


You can add properties to JavaScript objects. When this is done after creating an instance of an object, the object changes irreversibly. He mutates as one of the characters of the X-Men.

In the following example, the constant egg , an object, mutates after the isBroken property is added to it. Such objects (like egg ) we call mutable (that is, having the ability to mutate, change).
')
 const egg = { name: "Humpty Dumpty" }; egg.isBroken = false; console.log(egg); // { //   name: "Humpty Dumpty", //   isBroken: false // } 

Mutations are quite common in JavaScript. You can literally run into them anytime, anywhere.

About the danger of mutations


Suppose you have created a constant named newEgg in which an egg object is written. Then it was necessary to change the name property of newEgg :

 const egg = { name: "Humpty Dumpty" }; const newEgg = egg; newEgg.name = "Errr ... Not Humpty Dumpty"; 

When we change newEgg (subject the object to a mutation), the egg changes automatically. Did you know about this?

 console.log(egg); // { //   name: "Errr ... Not Humpty Dumpty" // } 

The above example illustrates the danger of mutations. It comes down to the fact that when you change something in the code, something that is somewhere else can also change, and you won't know about it. As a result, errors that are difficult to find and correct.

All these oddities are due to the fact that objects in JavaScript are passed by reference.

Objects in javascript and links to them


In order to realize the meaning of the statement “objects are passed by reference”, you first need to understand that each object in JavaScript has a unique identifier. When you assign an object to a variable, you associate a variable with the identifier of this object (that is, the variable now refers to the object) instead of writing the value of the object to the variable, copying it. That is why, comparing two different objects, even containing the same values ​​(or not containing them at all), we get false .

 console.log({} === {}); // false 

When, in the example above, the egg constant was assigned to the newEgg constant, a reference to the same object referenced by the egg constant was written to newEgg . Since egg and newEgg refer to the same object, when newEgg changes, egg changes automatically.

 console.log(egg === newEgg); // true 

Unfortunately, in situations similar to the one described, it is usually not necessary that what is written in one variable changes when it affects the other, as this leads to the wrong behavior of the code, which manifests itself when it is least expected. So, how to prevent object mutations? Before finding the answer to this question, it would be good to first find out what is immutable in JS, that is, unchanged.

Immunable Primitives


In JavaScript, primitives (we are talking about data types String , Number , Boolean , Null , Undefined , and Symbol ) are immutable. That is, you cannot change the structure of a primitive, you cannot add properties or methods to it. For example, when you try to add a new property to a primitive, absolutely nothing happens.

 const egg = "Humpty Dumpty"; egg.isBroken = false; console.log(egg); // Humpty Dumpty console.log(egg.isBroken); // undefined 

Keyword const and immunity


Many people think that variables (constants) declared using the const keyword are immutable. However, it is not.

Using the const keyword does not make what is written in a constant immutable. It only does not allow the constant to assign a new value.

 const myName = "Zell"; myName = "Triceratops"; // ERROR 

When, using the keyword const , an object is defined, its internal structure may well be changed. In the example with the egg object, even though egg is a constant created using the keyword const , this object does not protect against mutation.

 const egg = { name: "Humpty Dumpty" }; egg.isBroken = false; console.log(egg); // { //   name: "Humpty Dumpty", //   isBroken: false // } 

Preventing object mutations


In order to prevent mutations of objects, you can, when working with them, use the Object.assign method, which implements the operation of creating new objects by combining existing objects with assigning their properties to the resulting object.

Object Method Object.assign


The Object.assign structure allows Object.assign to combine two objects (or more objects), resulting in a single new object. You can use it like this:

 const newObject = Object.assign(object1, object2, object3, object4); 

The newObject will contain properties from all objects passed to 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 of an object that is located to the right in the argument list of Object.assign overwrites the property of an object that is in the list to the left.

 const smallCupWithEar = { volume: 300, hasEar: true }; const largeCup = { volume: 500 }; //     volume  ,  300   500 const myIdealCup = Object.assign(smallCupWithEar, largeCup); console.log(myIdealCup); // { //   volume: 500, //   hasEar: true // } 

However, be careful! When you combine two objects with Object.assign , the first object in the argument list is subject to mutations. Others are not.

 console.log(smallCupWithEar); // { //   volume: 500, //   hasEar: true // } console.log(largeCup); // { //   volume: 500 // } 

▍ Solving the problem of mutation when using Object.assign


As the first Object.assign object, Object.assign can pass a new object in order to prevent the mutation of existing objects. However, the first object (empty) is still subject to change, but there is nothing to worry about, since the mutation does not affect anything important.

 const smallCupWithEar = { volume: 300, hasEar: true }; const largeCup = { volume: 500 }; //        const myIdealCup = Object.assign({}, smallCupWithEar, largeCup); 

New object after performing this operation can be changed as you like. This will not affect previous objects.

 myIdealCup.picture = "Mickey Mouse"; console.log(myIdealCup); // { //   volume: 500, //   hasEar: true, //   picture: "Mickey Mouse" // } // smallCupWithEar   console.log(smallCupWithEar); // { volume: 300, hasEar: true } // largeCup   console.log(largeCup); // { volume: 500 } 

▍Object.assign and links to property objects


Another problem with Object.assign is that it performs a shallow merge of objects - it copies properties directly from one object to another. At the same time, it also copies references to objects that are properties of the objects being processed.

Consider this by example.

Suppose you bought a new sound system. You can control its power, set the volume, bass level and other parameters. This is what the standard system configuration looks like.

 const defaultSettings = { power: true, soundSettings: {   volume: 50,   bass: 20,   //   } }; 

Some of your friends love loud music, so you decided to make a preset that is guaranteed to put the whole house on your ears.

 const loudPreset = { soundSettings: {   volume: 100 } }; 

Then you invite your friends to a party. In order to bring the system to a working state and at the same time use both the standard settings and those where the volume is turned up to the maximum, you try to combine defaultSettings and loudPreset .

 const partyPreset = Object.assign({}, defaultSettings, loudPreset); 

However, turning on the music, you realize that the system with partyPreset sounds weird. The volume is good, but no bass at all. When you explore partyPreset , you are surprised to find that the bass settings are not here!

 console.log(partyPreset); // { //   power: true, //   soundSettings: { //     volume: 100 //   } // } 

This is due to the fact that JavaScript copies the object-property soundSettings by reference. As with defaultSettings , and loudPreset have a soundSettings object, the object that stands to the right in the Object.assign arguments is copied to the new object.

If you change partyPreset , loudPreset mutates accordingly - as evidence that the link to soundSettings from loudPreset was copied to loudPreset .

 partyPreset.soundSettings.bass = 50; console.log(loudPreset); // { //   soundSettings: { //     volume: 100, //     bass: 50 //   } // } 

Since Object.assign performs a surface merging of objects, in similar situations, when a new object is a combination of objects containing property objects, something else needs to be used. What? For example, the assignment library.

▍ Library assignment


Assignment is a small library created by Nicolas Bevacqua from Pony Foo (a valuable source of information on JS). It helps to perform deep merge of objects (deep merge) and at the same time not worry about mutations. Using assignment is the same as working with Object.assign , except that it uses a different method name.

 //       assignment const partyPreset = assignment({}, defaultSettings, loudPreset); console.log(partyPreset); // { //   power: true, //   soundSettings: { //     volume: 100, //     bass: 20 //   } // } 

The library copies the values ​​of all objects nested in other objects into a new object, which prevents existing objects from mutation.

If you now try to change any property in partyPreset.soundSettings , you will find that loudPreset does not change.

 partyPreset.soundSettings.bass = 50; // loudPreset   console.log(loudPreset); // { //   soundSettings { //     volume: 100 //   } // } 

The assignment library is just one of many tools that allow you to deeply merge objects. Other libraries, including lodash.assign and merge-options , can also help you with this. You can safely choose the one that you like best.

Is it always necessary to use deep merge instead of Object.assign?


Since you now know how to protect objects from mutations, you can intelligently use Object.assign . There is nothing wrong with this standard method if you know how to use it correctly.

However, if you need to work with objects that have nested properties, always try to use a deep merge of objects instead of Object.assign .

Ensuring the immunity of objects


Although the methods we discussed above can help protect objects from mutations, they do not guarantee the immunity of objects created with their help. If you make a mistake and use Object.assign when working with an object that has nested object properties, then you may have serious trouble.

In order to protect against this, it is necessary to provide a guarantee that the object will not mutate at all. To do this, you can use a library like ImmutableJS . This library gives an error when trying to change an object processed with its help.

In addition, you can use the Object.freeze method and the deep-freeze library. These two tools do not produce errors, but do not allow objects to mutate.

Object.freeze method and deep-freeze library


The Object.freeze method protects the object's own properties from changes.

 const egg = { name: "Humpty Dumpty", isBroken: false }; // ""  egg Object.freeze(egg); //          egg.isBroken = true; console.log(egg); // { name: "Humpty Dumpty", isBroken: false } 

However, this method will not help if you try to change an object that is a property of a “frozen” object, like defaultSettings.soundSettings.base .

 const defaultSettings = { power: true, soundSettings: {   volume: 50,   bass: 20 } }; Object.freeze(defaultSettings); defaultSettings.soundSettings.bass = 100; //    soundSettings  console.log(defaultSettings); // { //   power: true, //   soundSettings: { //     volume: 50, //     bass: 100 //   } // } 

To prevent the mutation of property objects, you can use the deep-freeze library, which recursively calls Object.freeze for all properties of the frozen object, which are objects.

 const defaultSettings = { power: true, soundSettings: {   volume: 50,   bass: 20 } }; //  " " (   deep-freeze) deepFreeze(defaultSettings); //      ,      defaultSettings.soundSettings.bass = 100; // soundSettings    console.log(defaultSettings); // { //   power: true, //   soundSettings: { //     volume: 50, //     bass: 20 //   } // } 

About rewriting values ​​and mutations


Do not confuse the entry in variables and in the properties of objects of new values ​​with a mutation.
When a new value is written to a variable, in fact, it changes what it indicates. In the following example, the value of the variable a changes from 11 to 100 .

 let a = 11; a = 100; 

With mutation, the object itself changes. The reference to an object written to a variable or constant remains the same.

 const egg = { name: "Humpty Dumpty" }; egg.isBroken = false; 

Results


Mutations are dangerous because they can disrupt the work of the code, and, to do so completely unnoticed and unpredictable. Even if you suspect that the cause of the problem is in a mutation, the search for a problem place is also a problem. Therefore, the best way to protect the code from unpleasant surprises is to ensure, from the moment objects are created, their protection against mutations.

In order to protect objects from mutations, you can use libraries like ImmutableJS and Mori.js , or use the standard JS Object.assign and Object.freeze .

Note that the Object.assign and Object.freeze can protect only their own object properties from changes. If you want to protect against mutations and properties that are themselves objects, you will need libraries like assignment or deep-freeze .

Dear readers! Have you encountered unexpected errors in JS applications caused by object mutations?

Source: https://habr.com/ru/post/346998/


All Articles