Cómo funciona JS: clases y herencia, transpilación en Babel y TypeScript

Las clases son una de las formas más populares de estructurar proyectos de software en estos días. Este enfoque de programación también se usa en JavaScript. Hoy publicamos una traducción de la parte 15 de la serie de ecosistemas JS. Este artículo discutirá varios enfoques para implementar clases en JavaScript, mecanismos de herencia y transpiración. Comenzaremos diciéndole cómo funcionan los prototipos y analizando varias formas de simular la herencia basada en clases en bibliotecas populares. A continuación, hablaremos sobre cómo, gracias a la transpilación, es posible escribir programas JS que utilicen funciones que no están disponibles en el idioma o, aunque existen en forma de nuevos estándares o propuestas que se encuentran en diferentes etapas de aprobación, aún no se implementan en JS- motores En particular, hablaremos sobre Babel y TypeScript y las clases ECMAScript 2015. Después de eso, veremos algunos ejemplos que demuestran las características de la implementación interna de clases en el motor V8 JS.
imagen


Revisar


En JavaScript, nos enfrentamos constantemente con objetos, incluso cuando parece que estamos trabajando con tipos de datos primitivos. Por ejemplo, cree un literal de cadena:

const name = "SessionStack"; 

Después de eso, podemos pasar inmediatamente al name para llamar a varios métodos de un objeto de tipo String , al que el literal de cadena que creamos se convertirá automáticamente.

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

A diferencia de otros lenguajes, en JavaScript, después de haber creado una variable que contiene, por ejemplo, una cadena o un número, podemos, sin realizar una conversión explícita, trabajar con esta variable como si se hubiera creado originalmente usando la new palabra clave y el constructor correspondiente. Como resultado, debido a la creación automática de objetos que encapsulan valores primitivos, puede trabajar con valores como si fueran objetos, en particular, consulte sus métodos y propiedades.

Otro hecho notable con respecto al sistema de tipo JavaScript es que, por ejemplo, las matrices también son objetos. Si observa el resultado del comando typeof para la matriz, puede ver que informa que la entidad bajo investigación tiene el tipo de datos del object . Como resultado, resulta que los índices de los elementos de la matriz son solo propiedades de un objeto en particular. Por lo tanto, cuando accedemos a un elemento de una matriz por índice, todo se reduce a trabajar con una propiedad de un objeto de tipo Array y obtener el valor de esta propiedad. Si hablamos de cómo se almacenan los datos dentro de objetos y matrices ordinarios, las siguientes dos construcciones conducen a la creación de estructuras de datos casi idénticas:

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

Como resultado, el acceso a los elementos de la matriz y a las propiedades del objeto se realiza a la misma velocidad. El autor de este artículo dice que descubrió en el curso de la resolución de un problema complejo. Es decir, una vez que necesitaba llevar a cabo una optimización seria de una pieza de código muy importante en el proyecto. Después de intentar muchos enfoques simples, decidió reemplazar todos los objetos utilizados en este código con matrices. En teoría, acceder a los elementos de la matriz es más rápido que trabajar con claves de tabla hash. Para su sorpresa, este reemplazo no afectó el rendimiento de ninguna manera, ya que trabajar con matrices y trabajar con objetos en JavaScript se reduce a interactuar con las teclas de tabla hash, lo que, en cualquier caso, requiere la misma cantidad de tiempo.

Simulando clases usando prototipos


Cuando pensamos en objetos, lo primero que viene a la mente son las clases. Quizás cada uno de los que se dedican a la programación de hoy creó aplicaciones cuya estructura se basa en las clases y en las relaciones entre ellas. Aunque los objetos en JavaScript se pueden encontrar literalmente en todas partes, el lenguaje no utiliza un sistema tradicional de herencia basado en clases. JavaScript usa prototipos para resolver problemas similares.


Objeto y su prototipo

En JavaScript, cada objeto está asociado con otro objeto, con su propio prototipo. Cuando intenta acceder a una propiedad o método de un objeto, la búsqueda de lo que necesita se realiza primero en el objeto mismo. Si la búsqueda no tiene éxito, continúa en el prototipo del objeto.

Considere un ejemplo simple que describe una función constructora para la clase base Component :

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

Aquí asignamos la función render() al método prototipo, ya que necesitamos cada instancia de la clase Component para usar este método. Cuando, en cualquier caso de Component , se llama al método de representación, su búsqueda comienza en el propio objeto para el que se llama. Luego, la búsqueda continúa en el prototipo, donde el sistema encuentra este método.


Prototipo y dos instancias de la clase Componente

Ahora intentemos extender la clase Component . InputField un constructor para una nueva clase - InputField :

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

Si necesitamos la clase InputField ampliar la funcionalidad de la clase Component y poder llamar a su método de render , debemos cambiar su prototipo. Cuando se llama a un método en una instancia de una clase secundaria, buscarlo en un prototipo vacío no tiene sentido. Necesitamos, en la búsqueda de este método, encontrarnos en la clase Component . Por lo tanto, debemos hacer lo siguiente:

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

Ahora, cuando trabaje con una instancia de la clase InputField y llame al método de la clase Component , este método se encontrará en el prototipo de la clase Component . Para implementar el sistema de herencia, debe conectar el prototipo InputField a una instancia de la clase Component . Muchas bibliotecas usan Object.setPrototypeOf () para resolver este problema.


Extendiendo la Clase Componente con la Clase InputField

Sin embargo, las acciones anteriores no son suficientes para implementar un mecanismo similar a la herencia tradicional. Cada vez que ampliamos la clase, debemos realizar las siguientes acciones:

  • Convierta el prototipo de la clase descendiente en una instancia de la clase padre.
  • Llame, en el constructor de la clase descendiente, al constructor de la clase principal para asegurarse de que la clase principal se inicialice correctamente.
  • Proporcione un mecanismo para llamar a los métodos de la clase principal en situaciones en las que la clase descendiente anula el método principal, pero es necesario llamar a la implementación original de este método desde la clase principal.

Como puede ver, si un desarrollador de JS quiere usar las capacidades de herencia basada en clases, tendrá que realizar constantemente los pasos anteriores. En el caso de que necesite crear muchas clases, todo esto puede hacerse en forma de funciones adecuadas para su reutilización.

De hecho, la tarea de organizar la herencia basada en clases se resolvió inicialmente en la práctica del desarrollo JS de esta manera. En particular, utilizando varias bibliotecas. Tales soluciones se hicieron muy populares, lo que indicaba claramente que faltaba algo en JavaScript. Es por eso que ECMAScript 2015 introdujo nuevas construcciones sintácticas destinadas a apoyar el trabajo con clases y a implementar los mecanismos de herencia correspondientes.

Transpilación de clase


Después de que se propusieron las nuevas características de ECMAScript 2015 (ES6), la comunidad de desarrolladores de JS quería usarlas lo antes posible, sin esperar la finalización del largo proceso de agregar soporte para estas características en los motores y navegadores JS. Al resolver tales problemas, la transpilación es buena. En este caso, la compilación se reduce a transformar el código JS escrito de acuerdo con las reglas de ES6 en una vista que sea comprensible para los navegadores que aún no admiten las capacidades de ES6. Como resultado, por ejemplo, es posible declarar clases e implementar mecanismos de herencia basados ​​en clases de acuerdo con las reglas de ES6 y convertir estas construcciones en código que funcione en cualquier navegador. Esquemáticamente, este proceso, usando el ejemplo de procesar una función de flecha por un transpilador (otra nueva función de lenguaje que necesita tiempo para ser compatible), se puede representar como se muestra en la figura a continuación.


Transpilacion

Uno de los transpiladores JavaScript más populares es Babel.js. Veamos cómo funciona realizando una compilación del código de declaración de la clase Component , del que hablamos anteriormente. Entonces aquí está el código ES6:

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

Y esto es en lo que se convierte este código después de la transpilación:

 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; }(); 

Como puede ver, el código ECMAScript 5 se obtiene a la salida del transpilador, que puede ejecutarse en cualquier entorno. Además, aquí se agregan las llamadas a algunas funciones que forman parte de la biblioteca estándar de Babel.

Estamos hablando de las _classCallCheck() y _createClass() incluidas en el código transpilado. La primera función, _classCallCheck() , está diseñada para evitar que la función constructora se llame como una función regular. Para hacer esto, verifica si el contexto en el que se llama la función es el contexto de instancia de la clase Component . El código verifica si la palabra clave this apunta a una instancia similar. La segunda función, _createClass() , crea propiedades de objeto que se le pasan como una matriz de objetos que contienen claves y sus valores.

Para entender cómo funciona la herencia, analizamos la clase InputField , que es el descendiente de la clase Component . Así es como se unen las relaciones de clase en ES6:

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

Aquí está el resultado de transpilar este código usando 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); 

En este ejemplo, la lógica de los mecanismos de herencia se encapsula en una llamada a la función _inherits() . Realiza las mismas acciones que describimos anteriormente, asociadas, en particular, a escribir en el prototipo de la clase descendiente una instancia de la clase padre.

Para transponer el código, Babel realiza varias de sus transformaciones. Primero, el código ES6 se analiza y se convierte en una representación intermedia llamada árbol de sintaxis abstracta . Luego, el árbol de sintaxis abstracta resultante se convierte en otro árbol, cada nodo del cual se transforma en su equivalente ES5. Como resultado, este árbol se convierte en código JS.

Árbol de sintaxis abstracta en Babel


Un árbol de sintaxis abstracta contiene nodos, cada uno de los cuales tiene solo un nodo primario. Babel tiene un tipo base para nodos. Contiene información sobre qué es el nodo y dónde se puede encontrar en el código. Existen varios tipos de nodos, por ejemplo, nodos para representar literales, como cadenas, números, valores null , etc. Además, hay nodos para representar expresiones utilizadas para controlar el flujo de ejecución del programa ( if construcción) y nodos para bucles ( for , while ). También hay un tipo especial de nodo para representar clases. Es un descendiente de la clase base Node . Extiende esta clase agregando campos para almacenar referencias a la clase base y al cuerpo de la clase como un nodo separado.
Convierta el siguiente fragmento de código en un árbol de sintaxis abstracta:

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

Así es como se verá su representación esquemática.


Árbol de sintaxis abstracta

Después de crear un árbol, cada uno de sus nodos se transforma en su nodo ES5 correspondiente, después de lo cual este nuevo árbol se convierte en código que se ajusta al estándar ECMAScript 5. Durante el proceso de conversión, primero encuentre el nodo que se encuentra más alejado del nodo raíz, después de lo cual este nodo se convierte en código utilizando fragmentos generados para cada nodo. Después de eso, el proceso se repite. Esta técnica se llama búsqueda profunda .

En el ejemplo anterior, el código para los dos nodos MethodDefinition se generará primero, después de lo cual se generará el código para el nodo ClassBody y, finalmente, el código para el nodo ClassDeclaration .

Transcripción de TypeScript


Otro sistema popular que utiliza la transpilación es TypeScript. Este es un lenguaje de programación cuyo código se transforma en código ECMAScript 5 que es comprensible para cualquier motor JS. Ofrece una nueva sintaxis para escribir aplicaciones JS. Aquí se explica cómo implementar la clase Component en TypeScript:

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

Aquí está el árbol de sintaxis abstracta para este código.


Árbol de sintaxis abstracta

TypeScript admite herencia.

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

Aquí está el resultado de la transpilación de este código:

 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)); 

Como puede ver, este es nuevamente un código ES5, en el que, además de las construcciones estándar, hay llamadas a algunas funciones desde la biblioteca TypeScript. Las capacidades de la función __extends() similares a las que mencionamos al principio de este material.

Gracias a la adopción generalizada de Babel y TypeScript, los mecanismos para declarar clases y organizar la herencia basada en clases se han convertido en herramientas estándar para estructurar aplicaciones JS. Esto contribuyó a la adición de soporte para estos mecanismos en los navegadores.

Soporte de clase de navegador


El soporte de clase apareció en el navegador Chrome en 2014. Esto permite que el navegador trabaje con declaraciones de clase sin el uso de la transpilación o cualquier biblioteca auxiliar.


Trabajando con clases en la consola Chrome JS

De hecho, el soporte del navegador para estos mecanismos no es más que azúcar sintáctica. Estas construcciones se convierten en las mismas estructuras básicas que ya son compatibles con el lenguaje. Como resultado, incluso si usa la nueva sintaxis, en un nivel inferior, todo se verá como crear constructores y manipular prototipos de objetos:


El apoyo de clase es azúcar sintáctico

Soporte de clase en V8


Hablemos sobre cómo funciona el soporte de clase ES6 en el motor V8 JS. En el artículo anterior sobre árboles de sintaxis abstracta, hablamos sobre el hecho de que al preparar el código JS para su ejecución, el sistema lo analiza y forma un árbol de sintaxis abstracta sobre su base. Al analizar construcciones de declaraciones de clase, los nodos de tipo ClassLiteral entran en el árbol de sintaxis abstracta.

Estos nodos almacenan un par de cosas interesantes. En primer lugar, es un constructor como una función separada y, en segundo lugar, es una lista de propiedades de clase. Pueden ser métodos, captadores, establecedores, campos públicos o privados. Dicho nodo, además, almacena una referencia a la clase padre, que extiende la clase para la cual se forma el nodo, que, nuevamente, almacena el constructor, la lista de propiedades y un enlace a su propia clase padre.

Después de que el nuevo nodo ClassLiteral transforma en código , se convierte en construcciones que consisten en funciones y prototipos.

Resumen


El autor de este material dice que SessionStack se esfuerza por optimizar el código de su biblioteca lo más completamente posible, ya que tiene que resolver tareas difíciles de recopilar información sobre todo lo que sucede en las páginas web. En el curso de la resolución de estos problemas, la biblioteca no debe ralentizar el trabajo de la página analizada. La optimización de este nivel requiere tener en cuenta los detalles más pequeños del ecosistema de JavaScript que afectan el rendimiento, en particular, teniendo en cuenta las características de cómo se organizan las clases y los mecanismos de herencia en ES6.

Estimados lectores! ¿Utiliza construcciones de sintaxis ES6 para trabajar con clases en JavaScript?

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


All Articles