📜 ⬆️ ⬇️

Unchangeable JavaScript: how it is done with ES6 and higher

Hello, dear readers. Today we would like to offer you a translation of the article on immutability in modern JavaScript. More information about the various features of ES6 is recommended to read in Kyle Simpson's wonderful book " ES6 and not only ."

Writing immutable Javascript code is correct. There are a number of amazing libraries, for example, Immutable.js , which could be useful for this. But is it possible today to do without libraries - to write on the “vanilla” JavaScript of a new generation?

In short - yes. In ES6 and ES.Next there are a number of amazing features that allow you to achieve unchangeable behavior without any fuss. In this article I will tell how to use them - it is interesting!

ES.Next is the following version (s) EcmaScript. New releases of EcmaScript are released annually and contain features that can be used today with the help of a transpiler, for example, Babel .
')

Problem


To begin, let us decide why immutability is so important? Well, if you change the data, it can be difficult to read code, prone to errors. If we are talking about primitive values ​​(for example, numbers and strings), it is quite simple to write “immutable” code - after all, these values ​​themselves cannot change. Variables containing primitive types always point to a specific value. If you pass it to another variable, the other variable will receive a copy of this value.

Objects (and arrays) have a different story: they are passed by reference . This means that if you pass an object to another variable, both of them will refer to the same object. If you later change the object belonging to any of them, then the changes will affect both variables. Example:

const person = { name: 'John', age: 28 } const newPerson = person newPerson.age = 30 console.log(newPerson === person) //  console.log(person) // { name: 'John', age: 30 } 

See what happens? By changing newObj , we automatically change the old variable obj . All because they refer to the same object. In most cases, this behavior is undesirable, and writing code like this is bad. Let's see how you can solve this problem.


We provide immutability


And what if you don’t pass the object and do not change it, but instead create a completely new object:

 const person = { name: 'John', age: 28 } const newPerson = Object.assign({}, person, { age: 30 }) console.log(newPerson === person) //  console.log(person) // { name: 'John', age: 28 } console.log(newPerson) // { name: 'John', age: 30 } 

Object.assign is an ES6 feature that allows you to take objects as parameters. It combines all the objects passed to it with the first. You may have wondered: why is the first parameter an empty object {} ? If the 'person' parameter was the first to go, we would still change the person . If we had written { age: 30 } , then we would again overwrite 30 with the value 28, since it would have gone later. Our solution works - the person has remained unchanged, since we dealt with it as unchanged!

Do you want to try these examples without unnecessary trouble? Open JSBin . In the left pane, click Javascript and replace it with ES6 / Babel. Everything, already can write on ES6 :).

However, in fact, EcmaScript has a special syntax that further simplifies such tasks. It is called object spread and can be used with the Babel transpiler. See:

 const person = { name: 'John', age: 28 } const newPerson = { ...person, age: 30 } console.log(newPerson === person) //  console.log(newPerson) // { name: 'John', age: 30 } 

The same result, only now the Code is still cleaner. First, the 'spread' (...) operator copies all properties from a person to a new object. Then we define the new property 'age' , which overwrites the old one. Observe the order: if age: 30 were defined above a person , then it would be overwritten by age: 28 .

And if you need to remove the item? No, we will not delete it, because the object would have changed again. This technique is a bit more complicated, and we could do, for example, like this:

 const person = { name: 'John', password: '123', age: 28 } const newPerson = Object.keys(person).reduce((obj, key) => { if (key !== property) { return { ...obj, [key]: person[key] } } return obj }, {}) 

As you can see, almost the entire operation has to be programmed independently. This functionality could be put at the forefront as a universal tool. But how is change and immutability applied with arrays?

Arrays


A small example: how to add an element to an array, changing it:

 const characters = [ 'Obi-Wan', 'Vader' ] const newCharacters = characters newCharacters.push('Luke') console.log(characters === newCharacters) //  :-( 

Same problem as with objects. We decidedly failed to create a new array, we just changed the old one. Fortunately, in ES6 there is a spread operator for the array! Here is how to use it:

 const characters = [ 'Obi-Wan', 'Vader' ] const newCharacters = [ ...characters, 'Luke' ] console.log(characters === newCharacters) // false console.log(characters) // [ 'Obi-Wan', 'Vader' ] console.log(newCharacters) // [ 'Obi-Wan', 'Vader', 'Luke' ] 

How simple! We have created a new array, which contains the old characters plus 'Luke', and the old array was not touched.

Consider how to do other operations with arrays without changing the original array:

 const characters = [ 'Obi-Wan', 'Vader', 'Luke' ] //   const withoutVader = characters.filter(char => char !== 'Vader') console.log(withoutVader) // [ 'Obi-Wan', 'Luke' ] //     const backInTime = characters.map(char => char === 'Vader' ? 'Anakin' : char) console.log(backInTime) // [ 'Obi-Wan', 'Anakin', 'Luke' ] //      const shoutOut = characters.map(char => char.toUpperCase()) console.log(shoutOut) // [ 'OBI-WAN', 'VADER', 'LUKE' ] //     const otherCharacters = [ 'Yoda', 'Finn' ] const moreCharacters = [ ...characters, ...otherCharacters ] console.log(moreCharacters) // [ 'Obi-Wan', 'Vader', 'Luke', 'Yoda', 'Finn' ] 

See how pleasant the “functional” operators are? The syntax of the arrow functions in ES6 only colors them. Each time such a function is started, such a function returns a new array, one exception is the ancient sorting method:

 const characters = [ 'Obi-Wan', 'Vader', 'Luke' ] const sortedCharacters = characters.sort() console.log(sortedCharacters === characters) //  :-( console.log(characters) // [ 'Luke', 'Obi-Wan', 'Vader' ] 

Yes I know. I believe that push and sort should act exactly like map , filter and concat , return new arrays. But they do not, and if you change something, you can probably break the Internet. If you need sorting, then perhaps you can use slice to get it all done:

 const characters = [ 'Obi-Wan', 'Vader', 'Luke' ] const sortedCharacters = characters.slice().sort() console.log(sortedCharacters === characters) // false :-D console.log(sortedCharacters) // [ 'Luke', 'Obi-Wan', 'Vader' ] console.log(characters) // [ 'Obi-Wan', 'Vader', 'Luke' ] 

There is a feeling that slice() is a bit of a hack, but it works.
As you can see, immutability is easily achieved using the most common modern JavaScript! In the end, the most important thing is common sense and understanding what exactly your code does. If programmed inadvertently, JavaScript can be unpredictable.

Performance note


What about performance? After all, to create new objects is a waste of time and memory? Yes, indeed, there are extra costs. But this disadvantage is more than offset by the benefits gained.

One of the most complex operations in JavaScript is tracking an object’s changes. Solutions like Object.observe(object, callback) pretty heavy. However, if you keep the state unchanged, you can do without oldObject === newObject and thus check whether the object has changed. Such an operation does not burden the CPU so much.

The second important advantage is that the quality of the code is improved. When you need to guarantee the immutability of the state, you have to better think through the structure of the entire application. You program more “functionally,” the whole code is easier to track, and the vile bugs in it get infrequent. Wherever you throw - wines everywhere, right?

For reference


» ES6 compatibility table:
» ES.Next compatibility table:

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


All Articles