📜 ⬆️ ⬇️

Immunity in JavaScript

habracut


What is Immunity?


Immutable (English immutable) is an object whose state can not be changed after creation. The result of any modification of such an object will always be a new object, while the old object will not change.


var mutableArr = [1, 2, 3, 4]; arr.push(5); console.log(mutableArr); // [1, 2, 3, 4, 5] //Use seamless-immutable.js var immutableArr = Immutable([1, 2, 3, 4]); var newImmutableArr = immutableArr.concat([5]); console.log(immutableArr); //[1, 2, 3, 4]; console.log(newImmutableArr); //[1, 2, 3, 4, 5]; 

This is not about deep copying: if an object has a nested structure, then all nested objects that have not been modified will be reused.


 //Use seamless-immutable.js var state = Immutable({ style : { color : { r : 128, g : 64, b : 32 }, font : { family : 'sans-serif', size : 14 } }, text : 'Example', bounds : { size : { width : 100, height : 200 }, position : { x : 300, y : 400 } } }); var nextState = state.setIn(['style', 'color', 'r'], 99); state.bounds === nextState.bounds; //true state.text === nextState.text; //true state.style.font === state.style.font; //true 

In memory, objects will be represented as follows:


In memory


Truth or lie? Immunity Data in JavaScript


Easy and quick change tracking


This feature is actively used in conjunction with today's popular VirtualDOM ( React , Mithril , Riot ) to speed up the redrawing of web pages.


Take the example of state , just above. After modifying the state object, you need to compare it with the nextState object and find out exactly what has changed in it. Immunity greatly simplifies the task for us: instead of comparing the value of each field of each nested state object with the corresponding value from the nextState , you can simply compare references to the corresponding objects and thus filter out the entire nested branches of comparisons.


 state === nextState //false state.text === nextState.text //true state.style === nextState.style //false state.style.color === nextState.style.color //false state.style.color.r === nextState.style.color.r //false state.style.color.g === nextState.style.color.g //true state.style.color.b === nextState.style.color.b //true state.style.font === nextState.style.font; //true //state.style.font.family === nextState.style.font.family; //true //state.style.font.size === nextState.style.font.size; //true state.bounds === nextState.bounds //true //state.bounds.size === nextState.bounds.size //true //state.bounds.size.width === nextState.bounds.size.width //true //state.bounds.size.height === nextState.bounds.size.height //true //state.bounds.position === nextState.bounds.position //true //state.bounds.position.x === nextState.bounds.position.x //true //state.bounds.position.y === nextState.bounds.position.y //true 

Inside the bounds and style.font objects, style.font comparison operation is necessary, since they are immutable, and the references to them have not changed.


Safer to use and easier to test.


There are often cases when the data passed to the function can be accidentally corrupted, and it is very difficult to track down such situations.


 var arr = [2, 1, 3, 5, 4, 0]; function render(items) { return arr .sort(function(a, b) {return a < b ? -1 : a > b ? 1 : 0}) .map(function(item){ return '<div>' + item + '</div>'; }); } render(arr); console.log(arr); // [0, 1, 2, 3, 4, 5] 

Here immobile data would save the situation. The sort function would be disabled.


 //Use seamless-immutable.js var arr = [2, 1, 3, 5, 4, 0]; function render(items) { return items .sort(function(a, b) {return a < b ? -1 : a > b ? 1 : 0}) .map(function(item){ return '<div>' + item + '</div>'; }); } render(arr); //Uncaught Error: The sort method cannot be invoked on an Immutable data structure. console.log(arr); 

Or would return a new sorted array without changing the old one:


 //Use immutable.js var arr = Immutable.fromJS([2, 1, 3, 5, 4, 0]); function render(items) { return arr .sort(function(a, b) {return a < b ? -1 : a > b ? 1 : 0}) .map(function(item){ return '<div>' + item + '</div>'; }); } render(arr); console.log(arr.toJS()); // [2, 1, 3, 5, 4, 0] 

Greater memory consumption


Each time an immutable object is modified, a copy of it is created with the necessary changes. This leads to more memory consumption than when working with a regular object. But since immutable objects never change, they can be implemented using a strategy called “structural sharing”, which generates a much lower cost of memory costs than one would expect. Compared to embedded arrays and objects, costs will still exist, but it will have a fixed value and can usually be compensated for by other benefits available due to immutability.


Easier to cache (memorize)


In most cases, it will not be easier to cache. This example will clarify the situation:


 var step_1 = Immutable({ data : { value : 0 } }); var step_2 = step_1.setIn(['data', 'value'], 1); var step_3 = step_2.setIn(['data', 'value'], 0); step_1.data === step_3.data; //false 

Despite the fact that data.value from the first step does not differ from data.value from the last step, the data object itself is different, and the link to it also changed.


No side effects


This is also not true:


 function test(immutableData) { var value = immutableData.get('value'); window.title = value; return immutableData.set('value', 42); } 

There is no guarantee that the function will become pure , or that it will have no side effects .


Acceleration code. More room for optimizations


Not everything is so obvious here, and performance depends on the concrete implementation of the immutable data structures with which you have to work. But if you take and simply freeze the object using Object.freeze , then access to it and its properties will not become faster, and in some browsers it will become even slower.


Thread safety


JavaScript is single-threaded, and there's nothing to talk about here. Many people confuse asynchrony and multithreading - this is not the same thing.
By default, there is only one thread that asynchronously serves the message queue.
In a browser for multithreading there is WebWorkers, but the only possible communication between threads is through sending strings or serialized JSON; it is not possible to refer to the same variables from different workers.


Language features


Keyword const


Using const instead of var or let does not mean that the value is constant or that it is immutable (immutable). The const keyword simply tells the compiler to ensure that no other value is assigned to the variable anymore.


If const used, modern JavaScript engines can perform a number of additional optimizations.


Example:


 const obj = { text : 'test'}; obj.text = 'abc'; obj.color = 'red'; console.log(obj); //Object {text: "abc", color: "red"} obj = {}; //Uncaught TypeError: Assignment to constant variable.(…) 

Object.freeze


The Object.freeze method freezes an object. This means that it prevents the addition of new properties to an object, the removal of old properties from an object, and the modification of existing properties or the values ​​of their enumeration, customizability, and recordability attributes. In essence, the object becomes effectively unchanged. The method returns a frozen object.


Third Party Libraries


Seamless-Immutable


The library offers immutable data structures that are backward compatible with regular arrays and objects. That is, access to values ​​by key or index will not differ from the usual, standard cycles will work, and all this can be used in conjunction with specialized high-performance libraries for data manipulation, such as Lodash or Underscore .


 var array = Immutable(["totally", "immutable", {hammer: "Can't Touch This"}]); array[1] = "I'm going to mutate you!" array[1] // "immutable" array[2].hammer = "hm, surely I can mutate this nested object..." array[2].hammer // "Can't Touch This" for (var index in array) { console.log(array[index]); } // "totally" // "immutable" // { hammer: 'Can't Touch This' } JSON.stringify(array) // '["totally","immutable",{"hammer":"Can't Touch This"}]' 

This library uses Object.freeze , and also prohibits the use of methods that can change data.


  Immutable([3, 1, 4]).sort() // This will throw an ImmutableError, because sort() is a mutating method. 

Some browsers, such as Safari, have performance problems when working with objects frozen using Object.freeze , so this is disabled in the production assembly for increased performance.


Immutable.js


Thanks to Facebook's promotion, this library for working with immunity data has become the most common and popular among web developers. It provides the following immutable data structures:



Mori


A library that adds persistent data structures from ClojureScript (Lists, Vectors, Maps, etc.) to JavaScript.


Differences from Immutable.js:



Usage example:


 var inc = function(n) { return n+1; }; mori.intoArray(mori.map(inc, mori.vector(1,2,3,4,5))); // => [2,3,4,5,6] //Efficient non-destructive updates! var v1 = mori.vector(1,2,3); var v2 = mori.conj(v1, 4); v1.toString(); // => '[1 2 3]' v2.toString(); // => '[1 2 3 4]' var sum = function(a, b) { return a + b; }; mori.reduce(sum, mori.vector(1, 2, 3, 4)); // => 10 //Lazy sequences! var _ = mori; _.intoArray(_.interpose("foo", _.vector(1, 2, 3, 4))); // => [1, "foo", 2, "foo", 3, "foo", 4] 

Development issues you will encounter


It will be about using Immutable.js (with Mori everything is also about). If you work with Seamless-Immutable , you will not have such problems due to backward compatibility with native JavaScript structures.


Work with server API


The fact is that in most cases the server API accepts and returns data in JSON format, which corresponds to standard objects and arrays from JavaScript. This means that it will be necessary to somehow convert the Immutable data to normal and vice versa.


Immutable.js for converting normal data into immutable offers the following function:


 Immutable.fromJS(json: any, reviver?: (k: any, v: Iterable<any, any>) => any): any 

where using the reviver function reviver can add your own conversion rules and manage existing ones.


Suppose the server API returns the following object to us:


 var response = [ {_id : '573b44d91fd2f10100d5f436', value : 1}, {_id : '573dd87b212dc501001950f2', value : 2}, {_id : '5735f6ae2a380401006af05b', value : 3}, {_id : '56bdc2e1cee8b801000ff339', value : 4} ] 

Most conveniently such an object will be represented as an orderedmap. reviver appropriate reviver :


 var state = Immutable.fromJS(response, function(k, v){ if(Immutable.Iterable.isIndexed(v)) { for(var elem of v) { if(!elem.get('_id')) { return elem; } } var ordered = []; for(var elem of v) { ordered.push([elem.get('_id'), elem.get('value')]); } return Immutable.OrderedMap(ordered); } return v; }); console.log(state.toJS()); //Object {573b44d91fd2f10100d5f436: 1, 573dd87b212dc501001950f2: 2, 5735f6ae2a380401006af05b: 3, 56bdc2e1cee8b801000ff339: 4} 

Suppose we need to change the data and send it back to the server:


 state = state.setIn(['573dd87b212dc501001950f2', 5]); console.log(state.toJS()); //Object {573b44d91fd2f10100d5f436: 1, 573dd87b212dc501001950f2: 5, 5735f6ae2a380401006af05b: 3, 56bdc2e1cee8b801000ff339: 4} 

Immutable.js for converting immobile data into regular data offers the following function:


 toJS(): any 

As you can see, the reviver missing, which means that you have to write your own external immutableHelper . And he should somehow be able to distinguish the usual OrderMap from the one that matches the structure of your source data. You cannot inherit from OrderMap either. In this application, the structures are likely to be nested, which will add to your additional difficulties.


You can, of course, use only List and Map in development, but then why is everything else? And what are the advantages of using Immutable.js specifically?


Immunity is everywhere


If the project used to work with native data structures, then it is not easy to switch to immutable data. We'll have to rewrite all the code that somehow interacts with the data.


Serialization / Deserialization


Immutable.js offers us nothing but functions fromJS , toJS , which work like this:


 var set = Immutable.Set([1, 2, 3, 2, 1]); set = Immutable.fromJS(set.toJS()); console.log(Immutable.Set.isSet(set)); //false console.log(Immutable.List.isList(set)); //true 

That is absolutely useless for serialization / deserialization.


There is a third-party library transit-immutable-js . An example of its use:


 var transit = require('transit-immutable-js'); var Immutable = require('immutable'); var m = Immutable.Map({with: "Some", data: "In"}); var str = transit.toJSON(m); console.log(str) // ["~#cmap",["with","Some","data","In"]] var m2 = transit.fromJSON(str); console.log(Immutable.is(m, m2));// true 

Performance


Benchmarks were written for performance testing. To run them at home, run the following commands:


 git clone https://github.com/MrCheater/immutable-benchmarks.git cd ./immutable-benchmarks npm install npm start 

The results of benchmarks can be seen on the graphs (repeats / ms). The longer the execution time, the worse the result.


When reading, native data structures and Seamless-immutable turned out to be the fastest.


Read


When recording was the fastest Mori. Seamless-immutable showed the worst result.


Write


Conclusion


This article will be useful to JavaScript developers who are faced with the need to use immune data in their applications to improve performance. In particular, this applies to frontend developers who work with frameworks using VirtualDOM ( React , Mithril , Riot ), as well as Flux / Redux solutions.


Summing up, it can be said that among the considered libraries for immobility in JavaScript the most fast, convenient and easy to use is Seamless-immutable . The most stable and common is Immutable.js . The fastest to write and the most unusual is Mori . I hope this study will help you choose a solution for your project. Good luck.


')

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


All Articles