Qué es V8 intérprete de JavaScirpt y cómo funciona. Cómo se ejecuta el código fuente JavaScript de una aplicación, explicado a bajo nivel. Cómo se comporta la ejecución de un código JavaScript de ejemplo paso a paso, explicando el mecanismo subyacente a bajo nivel de V8.
- V8 intérprete de JavaScript.
- Pipeline de compilación en V8.
- Ejecución de JavaScript.
- Ejemplo de código JavaScript y su ejecución subyacente.
V8 intérprete de JavaScript
V8 es el motor JavaScript y WebAssembly de código abierto y alto rendimiento de Google, escrito en el lenguaje de programación C++. Se utiliza en Chrome, Microsoft Edge, Opera, Brave y en Node.js, entre otros. Implementa ECMAScript y WebAssembly, y se ejecuta en sistemas Windows, macOS y Linux que utilizan procesadores x64, IA-32 o ARM. V8 se puede incrustar en cualquier aplicación de C++. V8 es el motor responsable de interpretar y ejecutar el código JavaScript.
Este estándar Ecma define el lenguaje ECMAScript 2025. ECMAScript se ha convertido en uno de los lenguajes de programación de propósito general más utilizados del mundo. Es conocido como el lenguaje incrustado en los navegadores web, pero también ha sido ampliamente adoptado para aplicaciones integradas y de servidor. ECMAScript se basa en varias tecnologías originarias, siendo las más conocidas JavaScript (Netscape) y JScript (Microsoft).
WebAssembly (abreviado Wasm) es un formato de código seguro, portátil y de bajo nivel diseñado para una ejecución eficiente y una representación compacta. Su objetivo principal es habilitar aplicaciones de alto rendimiento en la Web, pero no hace ninguna suposición específica ni proporciona características específicas de la Web, por lo que también se puede emplear en otros entornos.
V8 implementa ECMAScript y WebAssembly, y se ejecuta en sistemas Windows, macOS y Linux. V8 se puede incrustar en cualquier aplicación de C++.
V8 compila y ejecuta el código fuente de JavaScript, controla la asignación de memoria para objetos y recolecta elementos no utilizados de los objetos que ya no necesita. El recolector de basura es muy eficiente y es una de las claves del buen rendimiento de V8.
JavaScript se usa comúnmente para secuencias de comandos del lado del cliente en un navegador, y se usa para manipular objetos del Modelo de objetos de documento (DOM). Sin embargo, el DOM no suele ser proporcionado por el motor de JavaScript, sino por un navegador. Lo mismo ocurre con el V8: Google Chrome proporciona el DOM, pero el V8 proporciona todos los tipos de datos, operadores, objetos y funciones especificados en el estándar ECMA.
V8 permite que cualquier aplicación de C++ exponga sus propios objetos y funciones al código JavaScript. Dependerá del desarrollador decidir los objetos y funciones que quiera exponer a JavaScript.
Pipeline de compilación en V8
Cuando V8 comienza la ejecución del código, el código fuente JavaScript (JavaScript source code) se somete a un análisis, donde el analizador de V8 (parser) lo transforma en un árbol de sintaxis abstracta (AST o Abstract Syntax Tree). Esta estructura de árbol representa la organización jerárquica del código, lo que facilita el procesamiento del motor.
Posteriormente, el intérprete de Ignition de V8 se hace cargo del proceso de ejecución. El intérprete maneja el código línea por línea en un solo hilo. El subproceso tiene una memoria heap y una sola pila de llamadas (call stack), que es básicamente una estructura de datos LIFO que administra el contexto de ejecución y realiza un seguimiento de las funciones invocadas y el código en ejecución.
Ignition tiene dos objetivos, por un lado genera el bytecode y por el otro interpreta el código.
El Interpreter Ignition está en continua conversación con TurboFan para optimizar el código máquina (machine code) cuando sea necesario.
Ejecución de JavaScript
JavaScript es un lenguaje que se ejecuta línea a línea (thread of execution), es, por lo tanto, single thread.
Inicialmente, el intérprete crea un entorno especial para manejar el procesamiento y la ejecución de ese código. Este entorno se denomina contexto de ejecución (EC) y tiene dos partes: el contexto de ejecución global (global execution context) y el contexto de ejecución de la función (function execution context). El contexto de ejecución incluye el código que se está ejecutando actualmente junto con todos los elementos necesarios para su ejecución.
En realidad, la creación de un contexto de ejecución (EC) se produce en dos fases, que son la fase de creación de memoria y la fase de ejecución. En consecuencia, el motor V8 procesa el código dos veces.
El primer paso implica la creación de un contexto de ejecución global y una asignación de memoria (global memory). En la fase de creación de memoria, V8 pasa por todo el script y, básicamente, pone todo en la memoria, incluido el objeto global.
El segundo paso está dedicado a la ejecución del código. En esencia, durante la fase de ejecución, V8 vuelve a revisar el código para ejecutarlo línea por línea. Cada vez que encuentra una función, genera un nuevo contexto de ejecución de la función y la inserta en la pila de llamadas.
A lo largo de toda la canalización de ejecución, el contexto de ejecución global siempre se almacena en la parte inferior de la pila de llamadas y permanece allí. Mientras tanto, las funciones que se están ejecutando actualmente se apilan sobre él. Cuando una función termina su ejecución, se extrae de la pila de llamadas, lo que permite que el proceso se repita para las funciones posteriores.
Las definiciones de los conceptos anteriores:
Contexto de Ejecución Global (Global Execution Context)
- Contexto de Ejecución (Execution Context): es el entorno en el que se ejecuta el código. JavaScript tiene tres tipos de contextos de ejecución: el contexto global, el contexto de función y el contexto de evaluación.
- Global Execution Context: es el primer contexto que se crea cuando comienza la ejecución de un programa de JavaScript. Hay solo uno de estos por programa.
- Pila de Ejecución (Call Stack): es donde se apilan los contextos de ejecución. Cuando se llama a una función, se crea un nuevo contexto de ejecución y se añade a la pila. Cuando la función finaliza, se retira de la pila.
Hilo de Ejecución (Thread of Execution)
- Single Threaded: JavaScript es un lenguaje de un solo hilo, lo que significa que solo puede ejecutar una cosa a la vez. Este hilo es responsable de la ejecución del contexto global y de cualquier función que se llame.
Memoria Global (Global Memory)
- Memory Allocation: cuando se ejecuta el contexto global, se reservan espacios de memoria para todas las variables y funciones globales.
- Variables: se almacenan en el objeto global (en navegadores, esto es
window
). - Funciones: también se almacenan en el objeto global y pueden ser invocadas en cualquier momento.
- Variables: se almacenan en el objeto global (en navegadores, esto es
Para dejar aún más claro el concepto de ejecución de JavaScript, lo explicaremos a continuación paso a paso con un ejemplo de código.
Ejemplo de código JavaScript y su ejecución subyacente
En el siguiente código JavaScript de ejemplo:
1 2 3 4 5 6 7 8 9 10 |
const numero = 10; function multiplicaPor4 (numeroEntrada) { const resultado = numeroEntrada * 4; return resultado; } const numSalida = multiplicaPor4(numero); console.log(numSalida); const numSalida2 = multiplicaPor4(numero + 20); console.log(numSalida2); |
Explicamos paso a paso cómo realiza la compilación el motor V8.
En la primera línea se define una variable global llamada «numero», la cual pasará a la memoria global (numero = 10). En la segunda línea (hasta la sexta línea) definimos una función global llamada «multiplicaPor4», que también pasará a la memoria global, en string (multiplicaPor4 = -f-):
En la línea 7 definimos una variable «numSalida» que llama a la función «multiplicaPor4», pasándole como parámetro la variable global definida en la línea 1 «numero». En este momento, el motor V8 creará un contexto de ejecución para la función «multiplicaPor4». En este momento vuelve a la línea 2, donde está la definición de la función «multiplicaPor4». En este momento, se creará la variable «numeroEntrada» pero NO en la memoria global, si no en la memoria del contexto de ejecución (execution context) de la función (numeroEntrada = 10). Tras crear esta variable, siguiendo con la línea 4, se realizará la operación de multiplicar la variable por 4 y se pasará el resultado en la variable «resultado», que se creará en la memoria del contexto de ejecución de la función (resultado = 40):
En la línea 5, mediante el return, se devolverá el valor de la variable «resultado» a la memoria global y a la variable «numSalida», que es en la que se llamó a la función (numSalida = 40):
Dado que la función «multiplicaPor4» ya se ha usado, se destruye el contexto de ejecución de la función:
En la línea 8 mostraríamos el resultado por consola. En la línea 9 volvemos a llamar a la función multiplicaPor4, pero en este caso con la varaible «numSalida2» y pasándole «numero + 20». El motor V8 realizará los mismos pasos que para el caso anterior, creando un contexto de ejecución para la función multiplicaPor4, pasándole los nuevos valores, devolviendo el nuevo resultado y destruyendo el contexto de ejecución de la función.
El resultado por consola será: