Skip to content

Guía Completa del Entorno de Ejecución de JavaScript

Prefacio

Ya has aprendido la sintaxis básica de JavaScript, pero ¿alguna vez te has preguntado:

  • ¿Dónde se ejecuta realmente el código?
  • ¿Por qué el mismo código se comporta de manera diferente en el navegador y en Node.js?
  • ¿Por qué a veces el código se "congela" y otras veces parece ejecutarse "en paralelo"?

Este artículo te guiará a través del entorno de ejecución de JavaScript, incluyendo el bucle de eventos, la pila de llamadas, la gestión de memoria y más. Al terminar de leer, entenderás por qué el código se ejecuta en un orden determinado, podrás localizar rápidamente bugs relacionados con operaciones asíncronas, optimizar el rendimiento del código y evitar fugas de memoria.

¿Qué aprenderás en este artículo?

CapítuloContenido¿Qué podrás hacer después?
Capítulo 1Visión general del entorno de ejecuciónEntender dónde se ejecuta el código JavaScript
Capítulo 2Entorno de ejecución del navegadorConocer las Web APIs que ofrece el navegador
Capítulo 3Entorno de ejecución de Node.jsComprender el entorno JavaScript del lado del servidor
Capítulo 4Bucle de eventos en profundidadDominar el orden de ejecución de macrotareas y microtareas
Capítulo 5Pila de llamadas y memoriaEntender el proceso de ejecución del código y la gestión de memoria
Capítulo 6Técnicas prácticasOptimizar el rendimiento y depurar fugas de memoria

1. Visión General del Entorno de Ejecución

🤔 Pregunta clave

¿Qué es el "entorno de ejecución"? JavaScript es solo un lenguaje, ¿por qué el mismo código se comporta de manera diferente en distintos entornos?

1.1 Qué es el entorno de ejecución

Entorno de ejecución = Motor de JavaScript + APIs proporcionadas por el entorno

Si comparamos JavaScript con un "lenguaje de programación", el entorno de ejecución sería el "sistema operativo": determina lo que tu código puede y no puede hacer.

┌─────────────────────────────────────┐
│            Código JavaScript         │
├─────────────────────────────────────┤
│      Motor de JavaScript (V8)       │  ← Analiza y ejecuta el código
├─────────────────────────────────────┤
│   Entorno de ejecución              │
│   (Navegador / Node.js)             │  ← Proporciona capacidades adicionales
└─────────────────────────────────────┘

Una analogía: JavaScript es el "idioma", el entorno de ejecución es la "ciudad"

  • La sintaxis de JavaScript (el idioma) es la misma en todas partes
  • Pero las instalaciones que ofrece cada ciudad son diferentes:
    • Navegador = tiene DOM, window, fetch (como una ciudad con centros comerciales y bibliotecas)
    • Node.js = tiene fs, http, path (como una ciudad con fábricas y autopistas)

1.2 Los dos principales entornos de ejecución

CaracterísticaNavegadorNode.js
Uso principalInteracción web, interfaz de usuarioAplicaciones del lado del servidor, herramientas CLI
Objeto globalwindowglobal
API DOM✅ Soportada❌ No soportada
Sistema de archivos❌ Limitado✅ Soporte completo
Sistema de módulosES ModulesCommonJS + ES Modules
TemporizadoressetTimeout, setIntervalsetTimeout, setInterval
Peticiones de redfetch, XMLHttpRequestMódulos http, https

👇 Pruébalo tú mismo: Compara las diferencias entre el entorno del navegador y Node.js

运行时环境对比

浏览器环境

window
浏览器全局对象
window.location.href
document
DOM 操作
document.querySelector("h1")
localStorage
本地存储
localStorage.setItem("key", "value")
fetch
网络请求
fetch("/api/data")
setTimeout
定时器
setTimeout(() => {}, 1000)
特点:
  • ✅ 有 DOM 和 BOM API,可以操作网页
  • ✅ 有 Web Storage (localStorage, sessionStorage)
  • ✅ 有 fetch 和 XMLHttpRequest 进行网络请求
  • ❌ 没有文件系统访问权限
  • ❌ 不能直接创建 HTTP 服务器

代码演示:不同环境的差异

🌐浏览器结果
点击"在浏览器运行"查看结果
🟢Node.js 结果
需要在 Node.js 环境中运行

核心区别:

浏览器运行时专注于用户界面和网页交互,提供 DOM、BOM、fetch 等前端专用 API。

Node.js 运行时专注于服务器端开发,提供文件系统、HTTP 服务器、进程管理等后端专用 API。

同样的 JavaScript 语法,但能用的 API 完全不同——这就是"环境判断"的重要性。

💡 Idea clave

El entorno de ejecución determina qué APIs puedes usar. Las APIs DOM disponibles en el navegador no funcionan en Node.js; las APIs de archivos disponibles en Node.js tampoco funcionan en el navegador. Por eso algunos códigos necesitan "detección de entorno".


2. Entorno de Ejecución del Navegador

🤔 Pregunta clave

¿Qué capacidades ofrece el navegador para que JavaScript manipule páginas web?

2.1 Composición del entorno de ejecución del navegador

┌─────────────────────────────────────────────┐
│            Motor de JavaScript               │
│            (V8 / SpiderMonkey)               │
└─────────────────────────────────────────────┘

┌─────────────────────────────────────────────┐
│              Web APIs                        │
│  ┌─────────┐ ┌──────────┐ ┌──────────┐     │
│  │   DOM   │ │   BOM    │ │ Network  │     │
│  │Manipular │ │Controlar │ │Peticiones│     │
│  │  la web  │ │navegador │ │  de red  │     │
│  └─────────┘ └──────────┘ └──────────┘     │
└─────────────────────────────────────────────┘

┌─────────────────────────────────────────────┐
│           Bucle de eventos (Event Loop)      │
│  Coordina la ejecución de código, eventos    │
│  y la programación de tareas                 │
└─────────────────────────────────────────────┘

2.2 Las tres categorías de Web APIs

1. API DOM - Manipular el contenido de la página web

javascript
// Buscar un elemento
const title = document.querySelector('h1')

// Modificar el contenido
title.textContent = 'Nuevo título'

// Añadir estilos
title.style.color = 'red'

2. API BOM - Controlar el navegador

javascript
// Navegar a otra página
window.location.href = 'https://example.com'

// Almacenamiento del navegador
localStorage.setItem('key', 'value')

// Historial del navegador
history.back()

3. API Network - Peticiones de red

javascript
// Enviar una petición HTTP
fetch('/api/data')
  .then(response => response.json())
  .then(data => console.log(data))

2.3 Mecanismo de eventos propio del navegador

Una de las características más potentes del entorno de ejecución del navegador es estar "dirigido por eventos": el código no necesita ejecutarse continuamente, sino que se ejecuta cuando el usuario interactúa.

javascript
button.addEventListener('click', () => {
  console.log('Se ha hecho clic en el botón')
})

Tipos de eventos comunes:

Tipo de eventoCuándo se disparaEscenario real
clickClic del ratónInteracción con botones
inputCambio en el contenido del campo de entradaBúsqueda en tiempo real
scrollDesplazamiento de la páginaCarga diferida (lazy loading)
loadRecurso completamente cargadoInicialización de datos
errorCuando ocurre un errorManejo de errores

3. Entorno de Ejecución de Node.js

🤔 Pregunta clave

¿Qué permite que JavaScript se ejecute en el lado del servidor?

3.1 Composición de Node.js

┌─────────────────────────────────────────────┐
│            Motor de JavaScript               │
│                 (V8)                         │
└─────────────────────────────────────────────┘

┌─────────────────────────────────────────────┐
│           Módulos integrados de Node.js      │
│  ┌─────────┐ ┌──────────┐ ┌──────────┐     │
│  │   fs    │ │   http   │ │   path   │     │
│  │Operac.  │ │Servidor  │ │Manejo de │     │
│  │archivos │ │   web    │ │  rutas   │     │
│  └─────────┘ └──────────┘ └──────────┘     │
└─────────────────────────────────────────────┘

┌─────────────────────────────────────────────┐
│          Biblioteca libuv (bucle de eventos) │
│      Soporte de E/S asíncrona multiplataforma│
└─────────────────────────────────────────────┘

3.2 Capacidades exclusivas de Node.js

1. Operaciones del sistema de archivos

javascript
const fs = require('fs')

// Leer un archivo
fs.readFile('./data.txt', 'utf8', (err, data) => {
  if (err) throw err
  console.log(data)
})

// Escribir en un archivo
fs.writeFile('./output.txt', 'Hello', (err) => {
  if (err) throw err
  console.log('Escritura exitosa')
})

2. Servidor HTTP

javascript
const http = require('http')

const server = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/html' })
  res.end('<h1>Hello World</h1>')
})

server.listen(3000)

3. Sistema de módulos

javascript
// CommonJS (por defecto en Node.js)
const fs = require('fs')
module.exports = { myFunction }

// ES Modules (forma moderna)
import fs from 'fs'
export { myFunction }

3.3 Comparativa: Navegador vs Node.js

CaracterísticaNavegadorNode.js
Archivo de entradaArchivo HTMLArchivo JavaScript
Objeto globalwindow, documentglobal, process
Carga de módulosEtiqueta <script>require() / import
SeguridadEntorno aislado (sandbox), restringidoPuede acceder a recursos del sistema
UsoInterfaz de usuarioServicios backend, herramientas

4. Bucle de Eventos en Profundidad

🤔 Pregunta clave

JavaScript es de un solo hilo, ¿cómo logra no bloquearse?

4.1 Qué es el bucle de eventos

Bucle de eventos = El "centro de programación de tareas" de JavaScript

JavaScript es de un solo hilo, solo puede hacer una cosa a la vez. Pero el bucle de eventos hace que parezca capaz de hacer "muchas cosas a la vez".

Mecanismo central:

  1. Ejecutar código síncrono (pila de llamadas)
  2. Procesar tareas asíncronas (cola de tareas)
  3. Esperar nuevas tareas (ciclo continuo)
Pila de llamadas                  Cola de tareas
┌─────────────┐                  ┌──────────┐
│   Tarea 1   │                  │Macrotarea 1│
│   Tarea 2   │ ←──────────────── │Macrotarea 2│
│   Tarea 3   │  Al terminar una │Macrotarea 3│
└─────────────┘  se toma la      └──────────┘
      ↓           siguiente            ↑
      └────────────────────────────────┘
         El bucle de eventos comprueba constantemente

4.2 Macrotareas vs Microtareas

¡Este es el concepto que más fácilmente se confunde en entrevistas y en el desarrollo real!

Macrotareas (Macrotask):

  • setTimeout, setInterval
  • Operaciones de E/S
  • Renderizado de la UI

Microtareas (Microtask):

  • Promise.then
  • MutationObserver
  • queueMicrotask

Orden de ejecución: Código síncrono → Microtareas → Macrotareas

👇 Pruébalo tú mismo: Observa el orden de ejecución de macrotareas y microtareas

任务队列:宏任务 vs 微任务

代码示例

1console.log("1")同步
2setTimeout(() => console.log("2"), 0)宏任务
3Promise.resolve().then(() => console.log("3"))微任务
4console.log("4")同步
5setTimeout(() => console.log("5"), 0)宏任务

调用栈 (正在执行)

执行 console.log("1")

微任务队列 Microtask

队列为空

宏任务队列 Macrotask

队列为空

输出日志 (执行顺序)

等待输出...

执行顺序规则

1执行所有同步代码
2执行微任务队列中的所有任务
3执行一个宏任务
4重复步骤 2-3

核心要点: 微任务优先级高于宏任务。每次执行完一个宏任务后,都会检查并执行所有微任务,然后再执行下一个宏任务。

4.3 Pregunta clásica de entrevista

javascript
console.log('1')

setTimeout(() => console.log('2'), 0)

Promise.resolve().then(() => console.log('3'))

console.log('4')

// Salida: 1, 4, 3, 2

¿Por qué este orden?

  1. Ejecutar código síncrono: console.log('1'), console.log('4') → salida 1, 4
  2. Revisar la cola de microtareas: Promise.then → salida 3
  3. Revisar la cola de macrotareas: setTimeout → salida 2

💡 Consejo práctico

  • Si quieres que el código se ejecute lo antes posible, usa microtareas (Promise.then)
  • Si quieres retrasar la ejecución, usa macrotareas (setTimeout)
  • Nunca mezcles demasiadas operaciones asíncronas, o caerás en el "infierno de callbacks"

5. Pila de Llamadas y Memoria

🤔 Pregunta clave

¿Cómo se ejecuta el código? ¿Dónde se almacenan las variables? ¿Cuándo se recolectan?

5.1 Pila de llamadas: la "huella" de la ejecución de funciones

Pila de llamadas = El "bloc de notas" que registra las llamadas a funciones

Cada vez que se llama a una función, se añade un nuevo registro en la pila; cuando la función termina de ejecutarse, el registro se elimina.

javascript
function a() {
  b()
}

function b() {
  c()
}

function c() {
  console.log('Ejecución completada')
}

a()

Cambios en la pila de llamadas:

Paso 1: se llama a a()
┌─────────┐
│    a    │
└─────────┘

Paso 2: a() llama a b()
┌─────────┐
│    b    │
│    a    │
└─────────┘

Paso 3: b() llama a c()
┌─────────┐
│    c    │
│    b    │
│    a    │
└─────────┘

Paso 4: c() termina, se desapilan en orden
┌─────────┐
│    b    │
│    a    │
└─────────┘

👇 Pruébalo tú mismo: Observa los cambios en la pila de llamadas

调用栈:函数执行的足迹

代码

1main()
2function a() {
3function b() {
4function c() {
5console.log("执行完毕")
6}
7}
8}
9}

调用栈

栈底
栈为空
栈顶

当前状态:

调用 main()

输出

等待输出...

调用栈工作原理:

  • 每次调用函数,就会在栈上"压入"一个新的"栈帧"
  • 栈帧记录了函数的执行状态、局部变量等信息
  • 函数执行完毕,栈帧就会从栈上"弹出"
  • 栈是"后进先出"(LIFO)的数据结构
  • 如果递归太深,会导致"栈溢出"错误

调用栈就像一摞盘子:最后放上去的盘子最先被取走。每个函数就是一个盘子,执行完就取走,然后继续执行下面的函数。

5.2 Gestión de memoria: ¿a dónde va la basura?

JavaScript tiene un mecanismo de "recolección de basura" automático: no necesitas liberar memoria manualmente, el motor lo hace por ti.

Principio de la recolección de basura: algoritmo de marcado y barrido

  1. Fase de marcado: Comenzando desde la "raíz", encuentra todas las variables accesibles
  2. Fase de barrido: Las variables no marcadas son "basura" y se recolectan
javascript
// Ejemplo de recolección de basura
let obj1 = { name: 'Objeto 1' }
let obj2 = { name: 'Objeto 2' }

// obj1 se reasigna, el objeto original pierde su referencia
obj1 = null  // El { name: 'Objeto 1' } original será recolectado

// obj2 todavía está en uso, no será recolectado
console.log(obj2.name)

👇 Pruébalo tú mismo: Observa el proceso de recolección de basura

垃圾回收机制

标记阶段从根对象开始,标记所有可达对象
清除阶段回收未标记的对象

对象引用关系

未标记
已标记(可达)
已回收
🌳
Root
📦
obj1
📦
obj2
📦
obj3
📦
obj4
📦
obj5
📦
obj6
当前操作:从根对象开始标记

标记-清除算法 (Mark-and-Sweep)

1
标记阶段

从根对象(Root)开始,遍历所有可达对象,标记为"活动对象"

2
清除阶段

遍历整个堆内存,回收所有未被标记的对象

3
重置标记

清除所有标记位,为下一次垃圾回收做准备

核心要点
  • 根对象(Root): 全局变量、栈上的变量等,总是被认为是可达的
  • 可达对象: 从根对象出发,通过引用链能访问到的对象
  • 垃圾对象: 无法从根对象访问到的对象,会被回收
  • 循环引用: 如果两个对象互相引用但都不可达,仍会被回收

实际应用技巧

💡
及时解除引用

对象不再使用时,将其设为 null

🔒
避免意外的全局变量

使用 const/let 代替 var

🧹
清理事件监听

组件销毁时移除所有监听器

📊
定期检查内存

用 DevTools Memory 面板监控

5.3 Fugas de memoria: las consecuencias de olvidar limpiar

Fuga de memoria = Memoria que debería liberarse pero no se libera, acumulándose cada vez más

Causas comunes:

1. Demasiadas variables globales

javascript
// ❌ Error: las variables globales no se recolectan
globalCache = []

function addItem(item) {
  globalCache.push(item)
}

2. Listeners de eventos no eliminados

javascript
// ❌ Error: el listener no se ha eliminado
button.addEventListener('click', handleClick)

// ✅ Correcto: eliminar el listener cuando ya no se necesita
button.removeEventListener('click', handleClick)

3. Closures que referencian objetos grandes

javascript
// ❌ Error: el closure sigue referenciando un objeto grande, no se recolectará
function createHandler() {
  const bigData = new Array(1000000).fill('data')
  return function() {
    console.log('Procesando')
  }
}

const handler = createHandler()  // bigData permanece en memoria

👇 Pruébalo tú mismo: Observa cómo ocurren las fugas de memoria

内存泄漏演示

内存使用情况0%

全局变量泄漏

问题:全局变量不会被垃圾回收,会一直占用内存

示例:不断往全局数组添加数据,从不清理

全局变量 (0 项)
暂无全局变量
❌ 错误做法
// 全局变量不会被回收
globalCache = []
function addItem() {
  globalCache.push(largeData)
}

如何避免内存泄漏

  • 避免全局变量: 使用 const/let 代替 var,尽量使用局部变量
  • 及时清理监听器: 组件销毁时移除所有事件监听
  • 释放闭包引用: 不需要时将闭包变量设为 null
  • 使用 WeakMap/WeakSet: 自动清理不再被引用的对象
  • 定期检查: 用 DevTools Memory 面板检查内存泄漏

💡 Consejo práctico

  • Revisa periódicamente: Abre DevTools del navegador → Memory → Take Heap Snapshot para ver el uso de memoria
  • Evita variables globales: Usa const y let siempre que sea posible, no uses var
  • Limpia a tiempo: Elimina los listeners de eventos y temporizadores cuando ya no los necesites
  • Referencias débiles: Usa WeakMap y WeakSet para almacenar referencias a objetos

6. Técnicas Prácticas

🤔 Pregunta clave

¿Cómo escribir código JavaScript de alto rendimiento? ¿Cómo depurar cuando surgen problemas?

6.1 Técnicas de optimización de rendimiento

1. Reducir reflows y repaints

javascript
// ❌ Error: cada iteración del bucle dispara un reflow
for (let i = 0; i < 1000; i++) {
  element.style.top = i + 'px'
}

// ✅ Correcto: modificar en lote
element.style.transform = `translateY(${position}px)`

2. Usar delegación de eventos

javascript
// ❌ Error: añadir un listener a cada botón
buttons.forEach(btn => {
  btn.addEventListener('click', handleClick)
})

// ✅ Correcto: añadir un solo listener al elemento padre
container.addEventListener('click', (e) => {
  if (e.target.matches('.button')) {
    handleClick(e)
  }
})

3. Debounce y throttle

javascript
// Debounce: ejecutar después de que el usuario deje de escribir
function debounce(fn, delay) {
  let timer
  return function(...args) {
    clearTimeout(timer)
    timer = setTimeout(() => fn.apply(this, args), delay)
  }
}

// Throttle: limitar la frecuencia de ejecución
function throttle(fn, delay) {
  let lastTime = 0
  return function(...args) {
    const now = Date.now()
    if (now - lastTime >= delay) {
      fn.apply(this, args)
      lastTime = now
    }
  }
}

6.2 Técnicas de depuración

1. Usar DevTools para ver la pila de llamadas

javascript
function a() {
  b()
}

function b() {
  c()
}

function c() {
  debugger  // Pausa aquí para ver la pila de llamadas
}

a()

2. Usar console.trace() para rastrear la ruta de ejecución

javascript
function trackExecution() {
  console.trace('Ruta de ejecución')
  // Mostrará la pila de llamadas completa
}

3. Usar Performance para analizar el rendimiento

javascript
performance.mark('start')

// Ejecutar algún código
for (let i = 0; i < 10000; i++) {
  // ...
}

performance.mark('end')
performance.measure('Rendimiento del bucle', 'start', 'end')

const measure = performance.getEntriesByName('Rendimiento del bucle')[0]
console.log(`Tiempo de ejecución: ${measure.duration}ms`)

6.3 Consulta rápida de problemas comunes

ProblemaCausa posibleSolución
Alto uso de memoriaFuga de memoria, demasiada cachéRevisar variables globales, eliminar listeners
Página congeladaTareas largas bloquean el hilo principalDividir tareas, usar Web Workers
Eventos no se disparanListener no vinculado, elemento no existeVerificar el momento de carga del DOM
Orden asíncrono incorrectoMezcla de macrotareas y microtareasUnificar con Promise o async/await
Temporizadores imprecisosHilo principal bloqueadoUsar Web Workers o requestAnimationFrame

Resumen

Ahora deberías ser capaz de entender:

  • Entorno de ejecución = Motor + APIs del entorno, diferentes entornos ofrecen diferentes capacidades
  • El bucle de eventos coordina el orden de ejecución de código síncrono, microtareas y macrotareas
  • La pila de llamadas registra el proceso de ejecución de funciones, el desbordamiento de pila ocurre por recursión demasiado profunda
  • La recolección de basura limpia automáticamente las variables no utilizadas, pero hay que prestar atención a las fugas de memoria
  • La clave de la optimización de rendimiento es reducir reflows/repaints y usar la asincronía de forma adecuada

💡 Cuando tengas problemas, pregúntale a la IA así

  • "Esta función se ejecuta demasiado lento, ayúdame a ver cómo optimizar el rendimiento"
  • "El uso de memoria no para de subir, podría ser una fuga de memoria, ayúdame a revisarlo"
  • "El orden de las operaciones asíncronas es incorrecto, debería ser primero A y luego B, pero ahora A y B empiezan casi al mismo tiempo"
  • "El listener de eventos no se dispara, comprueba si el elemento ya está cargado en el DOM"