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

https://blog.sessionstack.com/how-javascript-works-the-internals-of-classes-and-inheritance-transpiling-in-babel-and-113612cdc220
  • Transfer
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,
image

[We advise you to read] Other 19 parts of the cycle
Часть 1: Обзор движка, механизмов времени выполнения, стека вызовов
Часть 2: О внутреннем устройстве V8 и оптимизации кода
Часть 3: Управление памятью, четыре вида утечек памяти и борьба с ними
Часть 4: Цикл событий, асинхронность и пять способов улучшения кода с помощью async / await
Часть 5: WebSocket и HTTP/2+SSE. Что выбрать?
Часть 6: Особенности и сфера применения WebAssembly
Часть 7: Веб-воркеры и пять сценариев их использования
Часть 8: Сервис-воркеры
Часть 9: Веб push-уведомления
Часть 10: Отслеживание изменений в DOM с помощью MutationObserver
Часть 11: Движки рендеринга веб-страниц и советы по оптимизации их производительности
Часть 12: Сетевая подсистема браузеров, оптимизация её производительности и безопасности
Часть 12: Сетевая подсистема браузеров, оптимизация её производительности и безопасности
Часть 13: Анимация средствами CSS и JavaScript
Часть 14: Как работает JS: абстрактные синтаксические деревья, парсинг и его оптимизация
Часть 15: Как работает JS: классы и наследование, транспиляция в Babel и TypeScript
Часть 16: Как работает JS: системы хранения данных
Часть 17: Как работает JS: технология Shadow DOM и веб-компоненты
Часть 18: Как работает JS: WebRTC и механизмы P2P-коммуникаций
Часть 19: Как работает JS: пользовательские элементы

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 the type Stringto which the string literal we have 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 newand 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 command typeofinvoked for the array, you can see that it reports that the entity being examined has a data typeobject. 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 element of an array by index, it comes down to working with the property of an object of type Arrayand 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 — its own 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 in which the constructor function for the base class is describedComponent:

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

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


The prototype and two instances of the Component

classComponent.We will now try to extend the class. Create a new class constructor -InputField:

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

If we need the class to InputFieldextend the functionality of the class Componentand have the ability to call its method render, 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 would be found in the class Component. Therefore, we need to do the following:

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

Now, when working with an instance of a class InputFieldand calling a class method Component, this method will be found in the class prototype Component. To implement the inheritance system, you need to connect a prototype InputFieldto a class instance Component. 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:

  • Make the child class prototype an instance of the parent class.
  • Call, in the constructor of the descendant class, the constructor of the parent class to ensure the correct initialization of the parent class.
  • Предусмотреть механизм вызова методов родительского класса в ситуациях, когда класс-потомок переопределяет родительский метод, но возникает необходимость в вызове исходной реализации этого метода из родительского класса.

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, on the example of processing the arrow function by the transpiler (another new language feature,


Transpilation

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

classComponent{
  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 () {
  functionComponent(content) {
    _classCallCheck(this, Component);
    this.content = content;
  }
  _createClass(Component, [{
    key: 'render',
    value: functionrender() {
      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 functions _classCallCheck()and _createClass()included in the transpiled code. The first function,, is _classCallCheck()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 the class instance Component. The code checks if the keyword points thisto a similar instance. The second function _createClass(),, creates the properties of an object, which are passed to it as an array of objects containing keys and their values.

In order to understand how inheritance works, let's analyze a class InputFieldthat is a class heir Component. Here is how class relationships are formed in ES6:

classInputFieldextendsComponent{
    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);
  functionInputField(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 function call _inherits(). 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, values null, and so on. In addition, there are nodes for representing expressions used to control the flow of program execution (construction if), and nodes for loops ( for, while). There is also a special type of node for representing classes. He 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:

classComponent{
  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 converted to 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 the two nodes will be generated firstMethodDefinition, then the code for the nodewill be generatedClassBody, and finally the code for the nodeClassDeclaration.

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 is how to implement a class Componenton TypeScript:

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

Here is the abstract syntax tree for this code.


Abstract

TypeScript syntax tree supports inheritance.

classInputFieldextendsComponent{
    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);
    functionInputField(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, besides the standard constructions, there are calls of some functions from the TypeScript library. The features of the function are __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 node is ClassLiteraltransformed 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?


Also popular now: