Esta web utiliza cookies para que podamos ofrecerte la mejor experiencia de usuario posible. La información de las cookies se almacena en tu navegador y realiza funciones tales como reconocerte cuando vuelves a nuestra web o ayudar a nuestro equipo a comprender qué secciones de la web encuentras más interesantes y útiles.
Motor de Javascript o JavaScript engine: ¿Cómo funciona? ¿Cómo evitar errores comunes? Te lo explicamos todo.
Antes de hablar del motor de JavaScript, es importante entender cómo funciona JavaScript por dentro porque nos permite crear un código optimizado y rápido de ejecución. Y no sólo eso, sino que también nos ayuda a entender la naturaleza de los errores que vemos por consola.
Es posible que alguna vez hayas perdido incontable tiempo de trabajo tratando de averiguar por qué una función se ejecuta antes que otra y, tal vez, hasta hayas puesto algo tan feo como esto:
</p> <!-- /wp:paragraph --> <!-- wp:paragraph --> <p>setTimeout( doSomething, 1000);</p> <!-- /wp:paragraph --> <!-- wp:paragraph --> <p>
Y pensaste que se ejecutaría un segundo más tarde, pero no lo hizo hasta mucho después… los tiempos de ejecución en JavaScript son misteriosos, ¿o tal vez no? Empecemos con un poco de historia.
Comienzos de la web y la irrupción de JavaScript
En la década de los noventa las conexiones a Internet no eran como las de ahora, la velocidad era de 56kbps y veíamos páginas web totalmente estáticas y sin capacidad de aportar soluciones de negocio empresariales más allá de información de texto plano, imágenes y animaciones vía GIF.
¿Cuándo nació JavaScript?
JavaScript nace en 1995 creado por Brendan Eich, trabajador por aquel entonces de Netscape, como proyecto interno para dotar a la web de dinamismo y velocidad.
Gracias al éxito y amplia aceptación de este lenguaje de scripting del lado del cliente, Microsoft lanzó en Internet Explorer su propio lenguaje “como un fork” de JavaScript, bautizado con el nombre de JScript, partiendo de JavaScript pero con algunas variaciones e implementaciones añadidas como, por ejemplo, el manejo de fechas para el “efecto 2000“, entre otros.
Tener distintas aproximaciones para un mismo fin se convirtió en un caos para la industria y, por lo tanto, para los usuarios. Algunas páginas web no funcionaban bien en todos los navegadores y para subsanar este comportamiento era necesario que programar varios códigos específicos con el impacto en costes extra que esto acarrea al equipo de desarrollo.
Para evitar una guerra de tecnologías finalmente Netscape envía la especificación de JavaScript a ECMA International para su estandarización. La primera versión oficial sería ECMAScript 1 y se publicó en 1997, con varias revisiones posteriores hasta ES6 o ES2015, momento en el que el estándar se va actualizando anualmente.
La siguiente infografía es muy interesante para ver de manera visual la evolución histórica de los navegadores, su ciclo de vida y en qué momento introducen cada tecnología.
Fuente: evolutionoftheweb.com
Después de este breve repaso por la historia nos vamos a centrar en el funcionamiento del motor de JavaScript. Este conocimiento es importante a la hora de depurar nuestras aplicaciones y para entender errores y comportamientos en la ejecución de nuestro código.
El motor de JavaScript
Para interpretar el lenguaje los navegadores incluyen un motor de JavaScript, siendo algunos de los más conocidos los siguientes:
- SpiderMonkey (Firefox)
- V8 usado navegadores basados en Chromium (Chrome, Microsoft Edge, Opera) y en Node.js y Deno.
- JavaScriptCore usado en navegadores basados en Webkit (Safari).
- Carakan (versiones antiguas de Opera).
- Chakra intérprete de JScript (Internet Explorer).
JavaScript es un lenguaje single-thread, es decir, tiene solo un hilo de ejecución, por lo que las tareas se van ejecutando secuencialmente.
El lanzamiento en 2008 del motor de JavaScript V8 como proyecto open source, desarrollado por Google, supuso una gran mejora en el motor de JavaScript al crear una combinación de intérprete y compilador. Actualmente el resto de motores modernos utilizan esta técnica, JIT (Just In Time compiler).
¿Cómo funciona el motor de JavaScript?
El compilador traduce el código fuente a bytecode de la manera más rápida posible y este bytecode es ejecutado por el intérprete. El sistema también cuenta con un «Profiler» que analiza las operaciones e identifica el código que se puede optimizar para mejorar el rendimiento. Luego se reemplazan las secciones de bytecode por el código optimizado. Esto implica que la velocidad de ejecución del código JavaScript mejora gradualmente mientras se ejecuta.
A continuación, vamos a desglosar algunos elementos fundamentales que necesitamos comprender para entender el funcionamiento interno del motor de JavaScript.
- Call stack
- Memory Heap
- Event loop
- Callback queue
- Web APIs
- Hoisting
Call Stack y Memory Heap | Para qué sirven
JavaScript tiene un solo subproceso con un contexto de ejecución global, esto significa que JavaScript maneja una sola pila de llamadas (Call Stack) y un Memory Heap, que hace referencia a la parte de la memoria no estructurada donde se guardan los objetos y funciones. Las funciones del Call Stack se procesarán en el orden en que se llama, comúnmente conocido como Last-In, First-Out (LIFO).
Veamos como funciona con un ejemplo muy sencillo utilizando las Dev Tools de Chrome. Primero creamos un ejemplo de código que realiza un cálculo con sumas y restas simples y ponemos un punto de parada para poder avanzar en la ejecución paso a paso.
Primero llama a la función «calculate» y la añade a la pila de ejecución; dentro de esta función se hace uso de una nueva función «addFive», y la añade al «Call Stack» encima de «calculate». Así mismo ocurre con «subtractTwo» que se coloca encima de las dos anteriores. Posteriormente, según se van ejecutando, se va vaciando en orden inverso a como se han añadido.
Web APIs con JavaScript
Hacen referencia a las APIs integradas en el navegador, están creadas con JavaScript y facilitan la implementación de algunas funcionalidades. Algunas de las interfaces más conocidas son:
- DOM (Document Object Model)
- XMLHttpRequest
- Geolocalización
- Funciones de timer (setTimeout, setInterval)
Event loop y Callback queue | Para qué sirven
Para manejar tareas que requieren de un mayor tiempo de procesamiento o tardan en devolver una respuesta, como por ejemplo, una llamada al servidor, se hace uso de los callbacks, funciones que se llaman cuando finalmente se recibe el resultado de la operación.
Los callbacks se van encolando en lo que llamamos “Callback Queue” en espera a ser pasados a la pila de ejecución.
El Event Loop es un observador que se encarga de escuchar si hay tareas pendientes en el Callback Queue y las añade al Call Stack cuando este se vacía.
El ejemplo más sencillo es la función setTimeout, recibe dos parámetros: el primero es la función callback a ejecutar y el segundo es el tiempo de espera hasta que es añadido a la cola; si hay más elementos en la cola se añadirán cuando se hayan ejecutado, el segundo parámetro indica el tiempo mínimo de espera por lo que no garantiza cuando se va ejecutar la tarea.
Veamos un pequeño ejemplo de cómo funciona el Event Loop, utilizando de nuevo la consola dentro del inspector web de Chrome, utilizando setTimeout e imprimiendo un mensaje en la pantalla.
Empieza a ejecutarse siguiendo los siguientes pasos:
- La primera línea de código es un console.log que se añade directamente al Call Stack
- La segunda línea se refiere a un setTimeout por lo que pasa la ejecución a las Web APIs que esperaran 2s antes de enviar la sentencia al Callback Queue
- La tercera línea se refiere a otro setTimeout pero en este caso el tiempo es 0 por lo que directamente se añade al Callback Queue
- La cuarta línea es otro console.log que se añade directamente al Call Stack
- La quinta línea se refiere a un setTimeout por lo que pasa la ejecución a las Web APIs que esperaran 1s antes de enviarlo el console.log al Callback Queue
- La sexta línea es otro console.log que se añade directamente al Call Stack
Hoisting y JavaScript
El término Hoisting se refiere al comportamiento de JavaScript por el que las declaraciones de variables y funciones son asignadas en memoria durante la fase de compilación, esto significa que se puede utilizar una variable / función antes de que sea declarada.
Por el contrario cuando se declaran variables con let y const aparece un error, ya que al contrario que var, solo están disponibles el scope en el que son declaradas y el hoisting no realiza la asignación de la variable a undefined.
Con el modo “estricto” de JavaScript no se permite que se utilicen variables antes de ser declaradas; en cualquier caso, es una buena práctica declarar las variables al inicio del scope.