JavaScript bajo el capó

Tabla de contenido
- Hilo y pila de llamadas
- Contexto de ejecución
- Bucle de eventos y JavaScript asíncrono
- Almacenamiento de memoria y recolección de basura
- Compilación JIT (Just In Time)
- Resumen
En este artículo, nos sumergiremos en el funcionamiento interno de JavaScript y cómo se ejecuta realmente. Al comprender los detalles, comprenderá el comportamiento de su código y, por lo tanto, podrá escribir mejores aplicaciones.
JavaScript se describe como:
Lenguaje de programación de subproceso único, recolección de elementos no utilizados, interpretado o compilado Just In Time con un bucle de eventos sin bloqueo.
Analicemos cada uno de estos términos clave.
Hilo y pila de llamadas:
El motor de JavaScript es un intérprete de subproceso único que consta de un montón y una pila de llamadas única que se utiliza para ejecutar el programa.
La pila de llamadas es una estructura de datos que utiliza el principio Último en entrar, primero en salir (LIFO) para almacenar y administrar temporalmente la invocación de funciones (llamada).
Significa que la última función que se inserta en la pila es la primera en aparecer cuando la función regresa.
Dado que la pila de llamadas es única, la ejecución de las funciones se realiza de una en una, de arriba a abajo. Significa que la pila de llamadas es síncrona.
Ahora, dado que es síncrono, se preguntará cómo puede JavaScript manejar llamadas asíncronas.
Bueno, el bucle de eventos es el secreto detrás de la programación asíncrona de JavaScript.
Pero antes de explicar el concepto de llamadas asíncronas dentro de JavaScript y cómo es posible con un lenguaje de subproceso único, comprendamos primero cómo se ejecuta el código.
Contexto de ejecución (EC):
El contexto de ejecución se define como el entorno en el que se ejecuta el código JavaScript.
La creación de un Contexto de Ejecución ocurre en dos fases:
1. Fase de Creación de Memoria:
- Crear el objeto global (que se denomina objeto de ventana en el navegador y objeto global en NodeJS).
- Crear el objeto "esto" y vincularlo al objeto global.
- Configuración del montón de memoria (un montón es una gran región de memoria, en su mayoría no estructurada) para almacenar variables y referencias de funciones.
- Almacenamiento de funciones y variables en contexto de ejecución global mediante la implementación de Hoisting .
Ahora que conocemos los pasos detrás de la ejecución del código, volvamos a la
Bucle de eventos:
Primero, comencemos mirando este diagrama:

Tenemos el motor que consta de dos componentes principales:
* Montón de memoria: aquí es donde ocurre la asignación de memoria.
* Pila de llamadas: aquí es donde se encuentran los marcos de la pila mientras se ejecuta el código.
Tenemos las API web, que son subprocesos a los que no puede acceder, solo puede llamarlos. Son las piezas del navegador en las que se activa la concurrencia, como DOM, AJAX, setTimeout y mucho más.
Finalmente, está la cola de devolución de llamada, que es una lista de eventos que se procesarán. Cada evento tiene una función asociada que se llama para manejarlo.
Entonces, ¿cuál es la tarea del bucle de eventos aquí?
El bucle de eventos tiene un trabajo simple: monitorear la pila de llamadas y la cola de devolución de llamadas. Si la pila de llamadas está vacía, el bucle de eventos tomará el primer evento de la cola y lo empujará a la pila de llamadas, que lo ejecuta de manera efectiva.
Tal iteración se denomina marca en el bucle de eventos. Cada evento es solo una función de devolución de llamada.
Almacenamiento de memoria y recolección de basura:
Para comprender la necesidad de la recolección de basura, primero debemos comprender el ciclo de vida de la memoria, que es prácticamente el mismo para cualquier lenguaje de programación, tiene 3 pasos principales.
1. Asigne la memoria.
2. Utilice la memoria asignada para leer, escribir o ambas cosas.
3. Libere la memoria asignada cuando ya no sea necesaria.
La mayoría de los problemas de administración de memoria ocurren cuando intentamos liberar la memoria asignada. La principal preocupación que surge es la determinación de los recursos de memoria no utilizados.
En el caso de los lenguajes de bajo nivel en los que el desarrollador tiene que decidir manualmente cuándo ya no se necesita la memoria, los lenguajes de alto nivel como JavaScript utilizan una forma automatizada de administración de memoria conocida como Garbage Collection (GC).
JavaScript utiliza dos estrategias famosas para realizar GC: la técnica de conteo de referencias y el algoritmo Mark-and-sweep.
Aquí hay una explicación detallada de MDN sobre ambos algoritmos y cómo funcionan.
Compilación JIT (Just In Time):
Volvamos a la definición de JavaScript: dice "lenguaje de programación interpretado y compilado por JIT", entonces, ¿qué significa eso? ¿Qué tal comenzar con la diferencia entre un compilador y un intérprete en general?
Como analogía, piensa en dos personas con diferentes idiomas que quieren comunicarse. Compilar es como detenerse y tomarse todo el tiempo para aprender el idioma, e interpretar será como tener a alguien ahí para interpretar cada oración.
Entonces, los lenguajes compilados tienen un tiempo de escritura lento y un tiempo de ejecución rápido y los lenguajes interpretados tienen lo contrario.
Hablando en términos técnicos: la compilación es un proceso de convertir el código fuente del programa en un código binario legible por máquina, antes de la ejecución, y un compilador toma todo el programa de una sola vez.
Por otro lado, un intérprete es un programa que ejecuta las instrucciones del programa sin necesidad de compilarlas previamente en un formato legible por máquina, y toma una sola línea de código a la vez.
Y aquí viene la función de compilación JIT que está mejorando el rendimiento de los programas interpretados. El código completo se convierte en código de máquina a la vez y luego se ejecuta inmediatamente .

Dentro del compilador JIT, tenemos un nuevo componente llamado monitor (también conocido como generador de perfiles). Ese monitor observa el código mientras se ejecuta y
- Identifique los componentes calientes o tibios del código, por ejemplo: código repetitivo.
- Transfiera esos componentes a código de máquina durante el tiempo de ejecución.
- Optimizar el código máquina generado.
- Intercambio en caliente de la implementación anterior del código.
Ahora que entendimos los conceptos básicos, tomemos un minuto para juntar todo y resumir los pasos que sigue JS Engine al ejecutar el código:

- El motor JS toma el código JS que está escrito en sintaxis legible por humanos y lo convierte en código de máquina.
- El motor utiliza un analizador para revisar el código línea por línea y verificar si la sintaxis es correcta. Si hay algún error, el código dejará de ejecutarse y se generará un error.
- Si se aprueban todas las comprobaciones, el analizador crea una estructura de datos de árbol denominada árbol de sintaxis abstracta (AST).
- El AST es una estructura de datos que representa el código en una estructura similar a un árbol. Es más fácil convertir el código en código de máquina desde un AST.
- Luego, el intérprete procede a tomar el AST y convertirlo en IR, que es una abstracción del código de máquina y un intermediario entre el código JS y el código de máquina. IR también permite realizar optimizaciones y es más móvil.
- Luego, el compilador JIT toma el IR generado y lo convierte en código de máquina, compilando el código, obteniendo comentarios sobre la marcha y usándolos para mejorar el proceso de compilación.
Gracias por leer :)
Puedes seguirme en Twitter y LinkedIn .