📜 ⬆️ ⬇️

Everything you need to know about prototypes, closures, and performance

Not so simple


At first glance, JavaScript may seem like a fairly simple language. Perhaps this is due to the rather flexible syntax. Or because of the similarity with other known languages, for example, with Java. Well or because of rather small amount of data types, in comparison with Java, Ruby, or .NET.

But in reality, the JavaScript syntax is much less simple and obvious than it might initially seem. Some of the most characteristic features of JavaScript are still misunderstood and not fully understood, especially among experienced developers. One of these features is performance when retrieving data (properties and variables) and performance problems that arise.

In JavaScript, data retrieval depends on two things: prototype inheritance and chains of scope. For the developer, an understanding of these two mechanisms is absolutely necessary, because it leads to an improvement in the structure, and often also in the performance of the code.

Getting properties in a prototype chain


When accessing a property in JavaScript, the entire chain of object prototypes is viewed.
')
Each function in JavaScript is an object. When a function is called with the new operator, a new object is created.

 function Person(firstName, lastName) { this.firstName = firstName; this.lastName = lastName; } var p1 = new Person('John', 'Doe'); var p2 = new Person('Robert', 'Doe'); 


In the example above, p1 and p2 two different objects, each of which is created using the Person constructor. As you can see from the following example, they are independent instances of Person :

 console.log(p1 instanceof Person); //  'true' console.log(p2 instanceof Person); //  'true' console.log(p1 === p2); //  'false' 


Once functions are in JavaScript objects, they can have properties. The most important property they have is called prototype .

prototype , which is an object, is inherited from the parent prototype again and again until it reaches the top level. This is often called a prototype chain . At the beginning of the chain is always the Object.prototype (that is, at the topmost level of the prototype chain); it contains the toString() , hasProperty() , isPrototypeOf() methods, and so on.



The prototype of each function can be extended by its own methods and properties.

When creating a new instance of an object (by calling a function with the new operator), it inherits all the properties through the prototype. However, keep in mind that instances do not have direct access to the prototype object, only its properties.

 //   Person    //  'getFullName': Person.prototype.getFullName = function() { return this.firstName + ' ' + this.lastName; } //  p1     console.log(p1.getFullName()); //  'John Doe' //   p1      'prototype'... console.log(p1.prototype); //  'undefined' console.log(p1.prototype.getFullName()); //   


This is an important and subtle point: even if p1 was created before defining the getFullName method, it will still have access to it, because its prototype was the prototype of Person .

(It is worth mentioning that browsers retain a reference to the prototype in the __proto__ property, but its use spoils karma, at least because it is not in ECMAScript standard , so do not use it ).

Since the Person p1 instance does not have direct access to the prototype object, we must rewrite the getFullName method in p1 like this:

 //    p1.getFullName, ** p1.prototype.getFullName, //  p1.prototype : p1.getFullName = function(){ return '  '; } 


Now p1 has its own property getFullName . But the p2 instance does not have its own implementation of this property. Accordingly, the call to p1.getFullName pulls the object's own method of p1 , while the call to p2.getFullName() goes up the prototype chain to Person .

 console.log(p1.getFullName()); //  '  ' console.log(p2.getFullName()); //  'Robert Doe' 


image

Another thing to fear is the ability to dynamically change the prototype of an object:

 function Parent() { this.someVar = 'someValue'; }; //   Parent,     'sayHello' Parent.prototype.sayHello = function(){ console.log('Hello'); }; function Child(){ //      //    . Parent.call(this); }; //   Child     'otherVar'... Child.prototype.otherVar = 'otherValue'; // ...       Child  Parent // (     'otherVar', //     Child  'otherVar'   ) Child.prototype = Object.create(Parent.prototype); var child = new Child(); child.sayHello(); //  'Hello' console.log(child.someVar); //  'someValue' console.log(child.otherVar); //  'undefined' 


When using prototype inheritance, remember that the properties of the child prototype should be set after inheriting from the parent object.



So, getting the properties in the prototype chain works like this:



Understanding how prototype inheritance works is generally important for developers, but beyond that it is important because of its influence (sometimes noticeable) on performance. As written in the V8 documentation, most JavaScript engines use a data structure similar to a dictionary to store properties. Therefore, calling any property requires a dynamic search to find the desired property. This method makes accessing properties in JavaScript much slower than accessing instance variables in languages ​​such as Java or Smalltalk.

Finding a variable through a chain of scopes


Another JavaScript search engine is based on a closure.

To understand how this works, you need to enter such a concept as the execution context .

In JavaScript, two types of execution context:



Execution contexts are organized in a stack. At the bottom of the stack is always a global context, unique to each program. Each time a function is encountered, a new execution context is created and placed at the beginning of the stack. As soon as the function is completed, its context is dropped from the stack.

 //   var message = 'Hello World'; var sayHello = function(n){ //   1      var i = 0; var innerSayHello = function() { //   2      console.log((i + 1) + ': ' + message); //   2   } for (i = 0; i < n; i++) { innerSayHello(); } //   1   }; sayHello(3); // : // 1: Hello World // 2: Hello World // 3: Hello World 


In each execution context, there is a special object called a scope chain that is used to resolve variables. The chain is essentially a stack of available execution contexts, from current to global. (To be more precise, the object at the top of the stack is called the Activation Object and contains: references to local variables for the executable function, given function arguments and two “special” objects: this and arguments ).



Notice how the default diagram points to the window object, and that the global object contains other objects, such as console and location .

When trying to resolve a variable through a chain of scopes, it first checks the current context for the desired variable. If no match is found, the next context object in the chain is checked, and so on, until the desired is found. If nothing is found, throw ReferenceError .

In addition, it is important to note that a new scope is added if try-catch or with blocks are encountered. In all these cases, a new object is created and placed at the top of the visibility chain.

 function Person(firstName, lastName) { this.firstName = firstName; this.lastName = lastName; }; function persist(person) { with (person) { //  'person'      //       "with",        // 'firstName'  'lastName',    person.firstName  // person.lastName if (!firstName) { throw new Error('FirstName is mandatory'); } if (!lastName) { throw new Error('LastName is mandatory'); } } try { person.save(); } catch(error) { //   ,   'error' console.log('Impossible to store ' + person + ', Reason: ' + error); } } var p1 = new Person('John', 'Doe'); persist(p1); 


In order to fully understand how a variable is resolved in a scope environment, it is important to remember that JavaScript does not currently have scope areas at the block level.

 for (var i = 0; i < 10; i++) { /* ... */ } // 'i'     ! console.log(i); //  '10' 


In most other languages, the code above will lead to an error, because the “life” (i.e. scope) of the variable i will be limited to a for block. But not in javascript. i is added to the Activation Object at the top of the scope chain, and remains there until the object is deleted, which happens after the execution context is removed from the stack. This behavior is known as floating variables.

It is worth mentioning that block-level scope support appeared in JavaScript with the addition of the new let keyword. It is already available in JavaScript 1.7 and should become an officially supported keyword starting with ECMAScript 6.

Performance impact


The way of finding and resolving variables and properties is one of the key features of JavaScript, but at the same time it is one of the most subtle and tricky points to understand.

The search operations that we have described along a chain of prototypes or scopes are repeated each time a property or variable is called. When this happens in a loop or during another heavy operation, you immediately feel the impact on code performance, especially against the background of the single-threaded nature of JavaScript, which prevents you from performing multiple operations at the same time.

 var start = new Date().getTime(); function Parent() { this.delta = 10; }; function ChildA(){}; ChildA.prototype = new Parent(); function ChildB(){} ChildB.prototype = new ChildA(); function ChildC(){} ChildC.prototype = new ChildB(); function ChildD(){}; ChildD.prototype = new ChildC(); function ChildE(){}; ChildE.prototype = new ChildD(); function nestedFn() { var child = new ChildE(); var counter = 0; for(var i = 0; i < 1000; i++) { for(var j = 0; j < 1000; j++) { for(var k = 0; k < 1000; k++) { counter += child.delta; } } } console.log('Final result: ' + counter); } nestedFn(); var end = new Date().getTime(); var diff = end - start; console.log('Total time: ' + diff + ' milliseconds'); 


In the code above, we have a long inheritance tree and three nested loops. In the deepest cycle, the counter is incremented by the value of the variable delta . But the delta value is determined at the very top of the inheritance tree! This means that each time you call child.delta entire tree is viewed from top to bottom . This can adversely affect performance.

Realizing this time, we can easily improve the performance of nestedFn by locally child.delta value of child.delta in the delta variable:

 function nestedFn() { var child = new ChildE(); var counter = 0; var delta = child.delta; // cache child.delta value in current scope for(var i = 0; i < 1000; i++) { for(var j = 0; j < 1000; j++) { for(var k = 0; k < 1000; k++) { counter += delta; // no inheritance tree traversal needed! } } } console.log('Final result: ' + counter); } nestedFn(); var end = new Date().getTime(); var diff = end - start; console.log('Total time: ' + diff + ' milliseconds'); 


Naturally, we can do this if we only know for sure that the value of child.delta will not change during the execution of loops; otherwise, we will have to periodically update the value of the variable with the current value.

So, let's run now both versions of nestedFn and see if there is a noticeable difference in performance between them.

 diego@alkadia:~$ node test.js Final result: 10000000000 Total time: 8270 milliseconds 


The execution took about 8 seconds. It's a lot.

Now let's see what with our optimized version:

 diego@alkadia:~$ node test2.js Final result: 10000000000 Total time: 1143 milliseconds 


This time, just a second. Much faster!

The use of local variables to prevent heavy queries is used both for finding properties (through a chain of prototypes) and for resolving variables (through scope).

Moreover, this “caching” of values ​​(i.e., in local variables) gives a gain when using some common JavaScript libraries. Take jQuery, for example. It supports “selectors,” a mechanism for obtaining one or more DOM elements. The ease with which this happens “helps” to forget how much the search on the selector is a difficult operation. Therefore, storing the search results in a variable gives a significant increase in performance.

 //     DOM  $('.container') "n"  for (var i = 0; i < n; i++) { $('.container').append(“Line “+i+”<br />”); } //  ... // ,    $('.container')  , //     DOM "n"  var $container = $('.container'); for (var i = 0; i < n; i++) { $container.append("Line "+i+"<br />"); } //      ... //     DOM  $('.container')  , //   DOM    var $html = ''; for (var i = 0; i < n; i++) { $html += 'Line ' + i + '<br />'; } $('.container').append($html); 


The second approach will give significantly better performance than the first, especially on pages with a large number of elements.

Summarize



Finding data in JavaScript is quite different from other languages, and there are a lot of nuances. Therefore, it is necessary to fully and most importantly correctly understand his concepts in order to truly know this language. This knowledge will bring cleaner, more reliable code and improved performance.

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


All Articles