How JS works: classes and inheritance, Babil and TypeScript transpiling

Nowadays, using classes is one of the most popular ways to structure software projects. This approach to programming is also used in JavaScript. Today we publish a translation of the 15th part of a series of materials on the JS ecosystem. This article focuses on different approaches to the implementation of classes in JavaScript, on the mechanisms of inheritance and transpilation. We begin with a story about how prototypes work and with analyzing various ways of imitating class-based inheritance in popular libraries. Next, we will talk about how, thanks to transpilation, you can write JS programs that use capabilities that are either missing from the language or, although they exist in the form of new standards or sentences that are at different stages of coordination, are not yet implemented in JS- engines. In particular, we will talk about Babel and TypeScript and ECMAScript 2015 classes. After that, we will examine several examples that demonstrate the features of the internal implementation of classes in the JS V8 engine.
image


Overview


In JavaScript, we constantly encounter objects, even when, seemingly, we work with primitive data types. For example, create a string literal:

const name = "SessionStack"; 

After this, we can immediately, referring to name , call various methods of an object of type String , to which the string literal we created will be automatically converted.

 console.log(name.repeat(2)); // SessionStackSessionStack console.log(name.toLowerCase()); // sessionstack 

Unlike other languages, in JavaScript, by creating a variable containing, for example, a string or number, we can, without carrying out an explicit conversion, work with this variable as if it was originally created using the keyword new and the corresponding constructor. As a result, by automatically creating objects that encapsulate primitive values, you can work with such values ​​as if they are objects, in particular, to refer to their methods and properties.

Another noteworthy fact regarding the JavaScript type system is that, for example, arrays are also objects. If you look at the output of the typeof command called for an array, you can see that it reports that the entity being examined has the object data type. As a result, it turns out that the indices of the elements of an array are just properties of a particular object. Therefore, when we refer to an array element by index, it comes down to working with the property of an object of type Array and getting the value of this property. If we talk about how data is stored inside ordinary objects and arrays, then the following two constructions lead to the creation of almost identical data structures:

 let names = ["SessionStack"]; let names = { "0": "SessionStack", "length": 1 } 

As a result, access to the elements of the array and to the properties of the object is performed at the same speed. The author of this article says that he figured it out in the course of solving one complex problem. Namely, once he needed to carry out a serious optimization of a very important code fragment in a project. After he had tried many simple approaches, he decided to replace all the objects used in this code with arrays. In theory, access to array elements is faster than working with hash table keys. To his surprise, this replacement had no effect on performance, since both working with arrays and working with objects in JavaScript is reduced to interacting with the keys of the hash table, which in the other case requires the same amount of time.

Imitation of classes using prototypes


When we think about objects, the first thing that comes to mind is classes. Perhaps, each of those who are engaged in programming today created applications whose structure is based on classes and on the relationship between them. Although objects in JavaScript can be found literally everywhere, the language does not use the traditional class-based inheritance system. In JavaScript, prototypes are used to solve similar problems.


Object and its prototype

In JavaScript, each object is associated with another object - with its prototype. When you try to access a property or method of an object, the search for what you need is first performed in the object itself. If the search was not successful, it continues in the prototype of the object.

Consider a simple example that describes the constructor function for the base class Component :

 function Component(content) { this.content = content; } Component.prototype.render = function() {   console.log(this.content); } 

Here we assign the render() function to the prototype method, since we need each instance of the Component class to use this method. When, in any instance of Component , the render method is called, its search begins in the object for which it is called. Then the search continues in the prototype, where the system finds this method.


A prototype and two instances of the Component class.

Let us now try to extend the Component class. Create a new class constructor - InputField :

 function InputField(value) {   this.content = `<input type="text" value="${value}" />`; } 

If we need the InputField class InputField extend the functionality of the Component class and be able to call its render method, we need to change its prototype. When a method is invoked for an instance of a child class, it makes no sense to look for it in an empty prototype. We need to, during the search for this method, it be found in the Component class. Therefore, we need to do the following:

 InputField.prototype = Object.create(new Component()); 

Now, when working with an instance of the class InputField and calling the method of the Component class, this method will be found in the prototype of the Component class. To implement the inheritance system, you need to connect the InputField prototype to an instance of the Component class. Many libraries use the Object.setPrototypeOf () method to solve this problem.


Extending the capabilities of the Component class using the InputField class

However, the above actions are not enough to implement a mechanism similar to traditional inheritance. Every time we expand a class, we need to do the following:


As you can see, if a JS developer wants to take advantage of class-based inheritance, he will constantly have to perform the actions described above. In the event that you need to create a set of classes, all this can be arranged in the form of functions that are suitable for repeated use.

In fact, the task of organizing class-based inheritance was initially solved in the practice of JS development in this way. In particular, through various libraries. Such solutions have become very popular, which clearly indicated that something was clearly not enough in JavaScript. That is why ECMAScript 2015 introduced new syntactic constructions aimed at supporting work with classes and at implementing the corresponding inheritance mechanisms.

Class transpiltation


After the new features of ECMAScript 2015 (ES6) were proposed, the JS-developers community wanted to take advantage of them as soon as possible, without waiting for the end of the lengthy process of adding support for these features to JS engines and browsers. In solving such problems, transpilation shows itself well. In this case, the transfusion is reduced to the transformation of JS-code, written according to the rules of ES6, to a form that is understandable to browsers, which so far do not support the capabilities of ES6. As a result, for example, it becomes possible to declare classes and implement inheritance mechanisms based on classes, according to the ES6 rules, and transform these constructs into code that works in any browsers. Schematically, this process, by the example of processing the arrow function by the transpiler (another new language feature, which needs time to support), can be represented as shown in the figure below.


Transpilation

One of the most popular JavaScript transpilers is Babel.js. Let's see how it works by performing the transpilation of the code for the declaration of the Component class, which we talked about above. So, here is the ES6 code:

 class Component { constructor(content) {   this.content = content; } render() { console.log(this.content) } } const component = new Component('SessionStack'); component.render(); 

But what this code turns into after transpilation:

 var Component = function () { function Component(content) {   _classCallCheck(this, Component);   this.content = content; } _createClass(Component, [{   key: 'render',   value: function render() {     console.log(this.content);   } }]); return Component; }(); 

As you can see, the output of the transpiler is ECMAScript 5-code, which can be run in any environment. In addition, there are added calls to some functions that are part of the standard library of Babel.

We are talking about the _classCallCheck() and _createClass() functions included in the transpiled code. The first function, _classCallCheck() , is designed so that the constructor function is not called as a normal function. To do this, it checks whether the context in which the function is called is the context of an instance of the Component class. The code checks whether the this keyword indicates a similar instance. The second function, _createClass() , is responsible for creating object properties that are passed to it as an array of objects containing keys and their values.

In order to understand how inheritance works, InputField analyze the class InputField , which is the successor of the class Component . Here is how class relationships are formed in ES6:

 class InputField extends Component {   constructor(value) {       const content = `<input type="text" value="${value}" />`;       super(content);   } } 

Here is the result of the transfiguration of this code with Babel:

 var InputField = function (_Component) { _inherits(InputField, _Component); function InputField(value) {   _classCallCheck(this, InputField);   var content = '<input type="text" value="' + value + '" />';   return _possibleConstructorReturn(this, (InputField.__proto__ || Object.getPrototypeOf(InputField)).call(this, content)); } return InputField; }(Component); 

In this example, the logic of the inheritance mechanisms is encapsulated in a call to the _inherits() function. It performs the same actions that we described above, related, in particular, to writing an instance of the parent class to the prototype class.

In order to translate the code, Babel performs several transformations. To begin with, the ES6 code is parsed and converted into an intermediate representation, called an abstract syntax tree . Then, the resulting abstract syntactic tree is transformed into another tree, each node of which is transformed into its ES5 equivalent. As a result, this tree is converted to JS-code.

Abstract syntax tree in Babel


An abstract syntax tree contains nodes, each of which has only one parent node. Babel has a base type for nodes. It contains information about what the node is and where it can be found in the code. There are different types of nodes, for example, nodes for representing literals, such as strings, numbers, null values, and so on. In addition, there are nodes for representing expressions used to control the flow of program execution ( if construct), and nodes for loops ( for , while ). There is also a special type of node for representing classes. It is a descendant of the base class Node . It extends this class by adding fields to store references to the base class and to the body of the class as a separate node.
Convert the following code snippet to an abstract syntax tree:

 class Component { constructor(content) {   this.content = content; } render() {   console.log(this.content) } } 

Here is what its schematic representation will look like.


Abstract syntax tree

After creating a tree, each node is transformed into its corresponding node ES5, after which this new tree is converted into code that complies with ECMAScript 5. During the conversion process, first find the node that is farthest from the root node, after which this node is converted into code using snippets generated for each node. After this process is repeated. This technique is called depth-first search .

In the above example, the code for two MethodDefinition nodes will first be generated, then the code for the ClassBody node will be generated, and finally the code for the ClassDeclaration node.

TypeScript Transpilation


Another popular system that uses transpilation is TypeScript. This is a programming language, the code on which is transformed into ECMAScript 5 code, understandable to any JS engine. It offers new syntax for writing JS applications. Here's how to implement the Component class in TypeScript:

 class Component {   content: string;   constructor(content: string) {       this.content = content;   }   render() {       console.log(this.content)   } } 

Here is the abstract syntax tree for this code.


Abstract syntax tree

TypeScript supports inheritance.

 class InputField extends Component {   constructor(value: string) {       const content = `<input type="text" value="${value}" />`;       super(content);   } } 

Here is what happens when you transpose this code:

 var InputField = /** @class */ (function (_super) {   __extends(InputField, _super);   function InputField(value) {       var _this = this;       var content = "<input type=\"text\" value=\"" + value + "\" />";       _this = _super.call(this, content) || this;       return _this;   }   return InputField; }(Component)); 

As you can see, we again have the ES5 code, in which, in addition to standard constructions, there are calls to some functions from the TypeScript library. The capabilities of the __extends() function __extends() similar to those we talked about at the very beginning of this material.

Thanks to the widespread use of Babel and TypeScript, the mechanisms for class declaration and class-based inheritance have become standard tools for structuring JS applications. This has contributed to adding support for these mechanisms to browsers.

Browser class support


Class support appeared in the Chrome browser in 2014. This allows the browser to work with class declarations without the use of transfiguration or any auxiliary libraries.


Working with classes in the Chrome JS console

In fact, browser support for these mechanisms is nothing more than “syntactic sugar”. These constructs are converted to the same basic structures that are already supported by the language. As a result, even if you use the new syntax, at a lower level, everything will look like the creation of constructors and manipulations with the prototypes of objects:


Class support is “syntactic sugar”

V8 class support


Let's talk about how the support of ES6 classes in the V8 JS engine works. In the previous article on abstract syntax trees, we said that when preparing a JS code for execution, the system produces its syntax analysis and forms an abstract syntax tree based on it. When parsing constructions of class declarations, nodes of type ClassLiteral fall into an abstract syntactic tree.

In such sites are stored a couple of interesting things. Firstly, this is a constructor as a separate function, and secondly, it is a list of class properties. These can be methods, getters, setters, public or private fields. Such a node also keeps a reference to the parent class, which extends the class for which the node is formed, which, again, stores the constructor, the list of properties, and a link to its own parent class.

After the new ClassLiteral node ClassLiteral transformed into code , it is transformed into constructions consisting of functions and prototypes.

Results


The author of this material says that the SessionStack company is striving to optimize the code of its library as fully as possible, since it has to solve difficult tasks of collecting information about everything that happens on web pages. In the course of solving these problems, the library should not slow down the work of the analyzed page. Optimization of this level requires taking into account the smallest details of the JavaScript ecosystem affecting performance, in particular, taking into account the features of how classes and inheritance mechanisms in ES6 are arranged.

Dear readers! Do you use ES6 syntax to work with classes in javascript?

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


All Articles