En la lección anterior diste por supuesto algo que no se había explicado: que el motor "sabe" qué variables existen en cada ámbito antes de ejecutar una sola línea. Esa preparación previa es lo que permite llamar a una declaración de función antes de escribirla, lo que hace que una var valga undefined en lugar de dar error, y lo que produce el Cannot access 'x' before initialization que ya has visto tres veces. En esta lección desmontarás ese mecanismo: las dos fases por las que pasa cada ámbito, el hoisting, la zona muerta temporal, el contexto de ejecución y la pila de llamadas que en 03-01 solo viste por encima. El objetivo práctico es doble: entender cualquier traza de error y ordenar el fichero js/app.js de Nómada Tareas para que nada de esto llegue a importar.

Contenido

  1. El motor no ejecuta a la primera pasada
  2. Fase de creación y fase de ejecución
  3. Hoisting de declaraciones de función
  4. Hoisting de var
  5. let y const: la zona muerta temporal
  6. Tabla comparativa completa
  7. Trazas comentadas paso a paso
  8. El contexto de ejecución global
  9. Los contextos de función
  10. La pila de llamadas
  11. RangeError: Maximum call stack size exceeded
  12. Leer un stack trace real
  13. Caso práctico: ordenar js/app.js
  14. Por qué el estilo moderno hace irrelevante el hoisting
  15. Errores Comunes y Consejos
  16. Ejercicios
  17. Conclusión

  1. El motor no ejecuta a la primera pasada

Intuitivamente uno imagina que JavaScript lee el fichero línea a línea y va ejecutando. No es así. Antes de ejecutar nada de un ámbito, el motor lo recorre entero para averiguar qué declaraciones contiene y reservarles sitio.

Este código lo demuestra:

console.log(pesoDePrioridad('alta'));    // 3   ✓ funciona
console.log(HOY);                        // ✗ ReferenceError: Cannot access 'HOY' before initialization

function pesoDePrioridad(prioridad) {
  return prioridad === 'alta' ? 3 : 1;
}

const HOY = '2026-09-20';

Fíjate en la asimetría: la función se puede llamar antes; la constante, no. Y —clave— el error de HOY no es "no está definida", sino "no se puede acceder todavía". El motor sabe perfectamente que HOY existe: simplemente no deja usarla aún. Eso solo tiene sentido si hubo una pasada previa.

  1. Fase de creación y fase de ejecución

Cada vez que el motor entra en un ámbito (el fichero completo o el cuerpo de una función), hace dos fases:

flowchart TD
    A["Entrar en un ámbito"] --> B["FASE DE CREACIÓN"]
    B --> B1["Registrar declaraciones de función<br/>completas y listas para usar"]
    B --> B2["Registrar las var<br/>e inicializarlas a undefined"]
    B --> B3["Registrar let/const<br/>SIN inicializar → TDZ"]
    B --> C["FASE DE EJECUCIÓN"]
    C --> C1["Recorrer las líneas de arriba abajo"]
    C --> C2["Asignar valores"]
    C --> C3["Llamar a funciones"]

Fase de creación (antes de ejecutar nada):

Tipo de declaración Qué se registra Valor inicial
function nombre() {} El nombre y la función completa La función, ya usable
var x El nombre undefined
let x / const x El nombre Nada: queda sin inicializar
class X {} El nombre Nada: TDZ, como let

Fase de ejecución: se recorren las líneas y se hacen las asignaciones reales.

El término hoisting ("elevación") describe el efecto observable de la fase de creación: es como si las declaraciones se hubieran movido al principio del ámbito. En realidad nada se mueve; el motor simplemente ya las conocía.

  1. Hoisting de declaraciones de función

Las declaraciones de función se elevan completas: nombre y cuerpo.

console.log(estaVencida('2026-09-05', 'pendiente', '2026-09-20'));   // true

function estaVencida(fechaLimite, estado, hoy) {
  return fechaLimite < hoy && estado !== 'hecha';
}

Es lo que en 03-01 se anunció de pasada: las declaraciones se pueden llamar antes de la línea donde están escritas. El motor "ve" así el fichero:

// --- Fase de creación ---
// estaVencida = [función completa]

// --- Fase de ejecución ---
console.log(estaVencida(...));   // ya existe

Las expresiones de función y las flechas no se comportan así, porque lo que se eleva es la variable, no la función:

console.log(typeof declarada);   // 'function'
console.log(typeof expresadaVar); // 'undefined'   ← la var existe, pero vale undefined
console.log(typeof expresadaConst); // ✗ ReferenceError

function declarada() {}
var expresadaVar = function () {};
const expresadaConst = () => {};

Un caso que sorprende: dentro de un bloque, en modo estricto, las declaraciones de función tienen ámbito de bloque.

'use strict';

if (true) {
  function auxiliar() { return 'dentro'; }
}
console.log(typeof auxiliar);   // 'undefined' en modo estricto

Conclusión práctica: no declares funciones dentro de bloques. Si necesitas una función condicional, usa una expresión asignada a una variable declarada fuera.

  1. Hoisting de var

Una var se eleva sin su valor: existe desde el principio del ámbito de función, valiendo undefined.

function calcularMargen(horasAbiertas) {
  console.log(limite);          // undefined  ← no da error, pero tampoco vale nada
  var limite = 40;
  console.log(limite);          // 40
  return limite - horasAbiertas;
}

calcularMargen(25);

Es equivalente a haber escrito:

function calcularMargen(horasAbiertas) {
  var limite;                   // ← elevada por la fase de creación
  console.log(limite);          // undefined
  limite = 40;                  // ← la asignación se queda donde estaba
  console.log(limite);
  return limite - horasAbiertas;
}

Este comportamiento es el origen de una clase entera de bugs silenciosos: el programa no falla, simplemente calcula con undefined y produce NaN más adelante. Combinado con el ámbito de función que viste en Ámbito y Closures, da lugar a situaciones como esta:

function analizar(estados) {
  for (var i = 0; i < estados.length; i++) {
    if (estados[i] === 'hecha') {
      var completadas = (completadas ?? 0) + 1;   // ¿funciona? sí, pero es ilegible
    }
  }
  return completadas;    // visible fuera del if y del for: ámbito de función
}

Con let y const nada de esto ocurre, porque el ámbito es el bloque y el acceso anticipado da error.

  1. let y const: la zona muerta temporal

let y const también se elevan: el motor registra sus nombres en la fase de creación. Lo que no hacen es inicializarse. Quedan en un estado especial llamado zona muerta temporal (Temporal Dead Zone, TDZ), que va desde el inicio del ámbito hasta la línea de la declaración.

{
  //  ┌─ inicio de la TDZ de `prioridad`
  console.log(prioridad);      // ✗ ReferenceError: Cannot access 'prioridad' before initialization
  const prioridad = 'alta';
  //  └─ fin de la TDZ
  console.log(prioridad);      // 'alta'
}
flowchart LR
    A["Inicio del ámbito"] -->|TDZ: acceder lanza ReferenceError| B["Línea de la declaración"]
    B -->|Inicializada| C["Resto del ámbito: uso normal"]

La TDZ no es un capricho: es una protección. Convierte en error inmediato lo que con var era un undefined silencioso. Y demuestra que la elevación existe, porque el mensaje distingue dos situaciones:

console.log(noDeclarada);      // ✗ ReferenceError: noDeclarada is not defined
console.log(enTdz);            // ✗ ReferenceError: Cannot access 'enTdz' before initialization
let enTdz = 1;

"is not defined" = el nombre no existe en ninguna parte. "Cannot access before initialization" = el nombre existe, pero aún está en su TDZ. Saber leer esa diferencia ahorra mucho tiempo depurando.

Un detalle final: typeof no protege de la TDZ.

console.log(typeof inexistente);   // 'undefined'  ← seguro
console.log(typeof enTdz);         // ✗ ReferenceError
let enTdz = 1;

  1. Tabla comparativa completa

function f(){} var x let x const x
¿Se eleva el nombre?
¿Se eleva el valor? Sí, completo No (undefined) No (TDZ) No (TDZ)
Uso antes de la línea Funciona undefined ReferenceError ReferenceError
Ámbito Bloque (estricto) Función Bloque Bloque
Redeclarable No No
Reasignable No
Inicialización obligatoria No No
Se añade a globalThis (nivel superior) No No

  1. Trazas comentadas paso a paso

Analicemos un fichero completo simulando lo que hace el motor. Este es el código:

'use strict';

console.log('1:', typeof pesoDePrioridad);   // ?
console.log('2:', typeof formatear);         // ?
console.log('3:', contadorTareas);           // ?
// console.log('4:', HOY);                   // lanzaría ReferenceError

var contadorTareas = 0;
const HOY = '2026-09-20';

function pesoDePrioridad(prioridad) {
  return prioridad === 'alta' ? 3 : 1;
}

const formatear = (t) => t.toUpperCase();

console.log('5:', typeof formatear);         // ?
console.log('6:', contadorTareas);           // ?
console.log('7:', HOY);                      // ?

Fase de creación del ámbito global:

Nombre Estado tras la fase de creación
pesoDePrioridad Función completa, lista
contadorTareas undefined (es var)
HOY Sin inicializar (TDZ)
formatear Sin inicializar (TDZ)

Fase de ejecución, línea a línea:

1: 'function'      → la declaración estaba lista desde el principio
2: 'undefined'     → ✗ NO: formatear está en TDZ; typeof LANZA ReferenceError

Aquí conviene detenerse, porque el ejemplo tiene una trampa deliberada: la línea 2 no imprime 'undefined', lanza un error, porque typeof no protege de la TDZ. Corrigiendo el fichero para que se ejecute:

'use strict';

console.log('1:', typeof pesoDePrioridad);   // 1: function
console.log('3:', contadorTareas);           // 3: undefined

var contadorTareas = 0;
const HOY = '2026-09-20';

function pesoDePrioridad(prioridad) {
  return prioridad === 'alta' ? 3 : 1;
}

const formatear = (t) => t.toUpperCase();

console.log('5:', typeof formatear);         // 5: function
console.log('6:', contadorTareas);           // 6: 0
console.log('7:', HOY);                      // 7: 2026-09-20

Salida completa:

1: function
3: undefined
5: function
6: 0
7: 2026-09-20

La moraleja del ejercicio no es memorizar la tabla, sino esto: cualquier fichero que dependa de estas sutilezas está mal ordenado.

  1. El contexto de ejecución global

Un contexto de ejecución es la estructura interna que el motor crea para ejecutar un trozo de código. Contiene tres cosas:

Componente Qué guarda
Entorno de variables Las variables y funciones declaradas en ese ámbito
Referencia al entorno exterior El eslabón que forma la cadena de ámbitos de 03-04
Valor de this El objeto de contexto (materia de 04-02)

El primero que se crea es el contexto global, uno por fichero o script. En el navegador, su this de nivel superior es window (o undefined dentro de funciones en modo estricto); en Node.js es module.exports o {} según el tipo de módulo.

Una consecuencia práctica que se ve enseguida:

var conVar = 'accesible desde window';
let conLet = 'no accesible desde window';

// En el navegador, en un <script> clásico:
console.log(window.conVar);   // 'accesible desde window'
console.log(window.conLet);   // undefined

Por eso var a nivel global es peligrosa: contamina el objeto global y puede pisar propiedades existentes. let y const no lo hacen.

  1. Los contextos de función

Cada llamada —no cada función, cada llamada— crea un contexto nuevo con sus propias dos fases.

function resumirCarga(responsable, horas) {
  //  Fase de creación de ESTE contexto:
  //    responsable = 'Iván', horas = 25   (los parámetros ya vienen asignados)
  //    margen      = sin inicializar (TDZ)
  //    etiqueta    = sin inicializar (TDZ)

  const margen = 40 - horas;
  const etiqueta = margen < 0 ? 'SOBRECARGA' : 'OK';
  return `${responsable}: ${margen} h de margen — ${etiqueta}`;
}

console.log(resumirCarga('Iván', 25));    // Iván: 15 h de margen — OK
console.log(resumirCarga('Lucía', 14));   // Lucía: 26 h de margen — OK

Las dos llamadas crean dos contextos completamente independientes, con sus propias margen y etiqueta, que se destruyen al terminar. Ese es exactamente el mecanismo que explicaba en 03-04 por qué las variables locales no se comparten entre llamadas… salvo que un closure las mantenga vivas.

  1. La pila de llamadas

Los contextos se apilan. La pila de llamadas (call stack) es la estructura donde el motor guarda qué contextos están activos, con la regla LIFO: el último en entrar es el primero en salir.

'use strict';

function pesoDePrioridad(prioridad) {
  if (prioridad === 'alta') return 3;
  if (prioridad === 'media') return 2;
  return 1;
}

function esfuerzoDeTarea(prioridad, horas) {
  return pesoDePrioridad(prioridad) * horas;
}

function informeBacklog(prioridades, horas) {
  let total = 0;
  for (let i = 0; i < horas.length; i++) {
    total += esfuerzoDeTarea(prioridades[i], horas[i]);
  }
  return total;
}

console.log(informeBacklog(
  ['alta', 'media', 'alta', 'baja', 'media', 'alta'],
  [12, 6, 14, 3, 8, 5]
));   // 124
sequenceDiagram
    participant G as Contexto global
    participant I as informeBacklog
    participant E as esfuerzoDeTarea
    participant P as pesoDePrioridad

    G->>I: llamada
    activate I
    loop 6 veces
        I->>E: llamada
        activate E
        E->>P: llamada
        activate P
        P-->>E: devuelve el peso
        deactivate P
        E-->>I: devuelve peso × horas
        deactivate E
    end
    I-->>G: devuelve 124
    deactivate I

En el punto más profundo hay cuatro contextos apilados: global → informeBacklogesfuerzoDeTareapesoDePrioridad. Y solo cuatro: el bucle no acumula contextos, porque cada llamada se cierra antes de la siguiente.

Dos propiedades importantes de la pila:

  1. Es de un solo hilo. JavaScript ejecuta una cosa cada vez. Mientras haya algo en la pila, nada más puede correr: ni un temporizador, ni un clic. Esa restricción es la razón de ser del bucle de eventos que estudiarás en El Bucle de Eventos y las Microtareas.
  2. Tiene un tamaño limitado. Y de ahí sale el error del apartado siguiente.

  1. RangeError: Maximum call stack size exceeded

Si las llamadas se apilan sin que ninguna termine, la pila se llena:

function contarTareasRestantes(n) {
  return contarTareasRestantes(n - 1);   // nunca para
}

contarTareasRestantes(6);
// ✗ RangeError: Maximum call stack size exceeded

El límite ronda las 10 000-15 000 llamadas según el motor. Las tres causas habituales:

Causa Ejemplo Solución
Recursión sin caso base El de arriba Añadir la condición de parada
Caso base inalcanzable if (n === 0) llamado con n = 5.5 Usar n <= 0
Recursión mutua accidental a() llama a b(), que llama a a() Revisar el ciclo

Puedes comprobar el límite de tu entorno:

function medirProfundidad(n = 1) {
  try {
    return medirProfundidad(n + 1);
  } catch (error) {
    return n;
  }
}

console.log(`Profundidad máxima: ${medirProfundidad()}`);
// Profundidad máxima: 11373  (varía según el motor y el navegador)

Este error es la señal más frecuente de una recursión mal escrita, y por eso volverás a él en Recursividad.

  1. Leer un stack trace real

Cuando algo falla, el stack de un Error es una foto de la pila en ese instante. Provoquemos un fallo real:

'use strict';

function pesoDePrioridad(prioridad) {
  return prioridad.toLowerCase() === 'alta' ? 3 : 1;   // ← revienta si es null
}

function esfuerzoDeTarea(prioridad, horas) {
  return pesoDePrioridad(prioridad) * horas;
}

function informeBacklog(prioridades, horas) {
  let total = 0;
  for (let i = 0; i < horas.length; i++) {
    total += esfuerzoDeTarea(prioridades[i], horas[i]);
  }
  return total;
}

console.log(informeBacklog(['alta', null], [12, 6]));

Traza en Node.js:

TypeError: Cannot read properties of null (reading 'toLowerCase')
    at pesoDePrioridad (/nomada-tareas/js/app.js:4:20)
    at esfuerzoDeTarea (/nomada-tareas/js/app.js:8:10)
    at informeBacklog (/nomada-tareas/js/app.js:14:14)
    at Object.<anonymous> (/nomada-tareas/js/app.js:19:13)

Cómo se lee, de arriba abajo:

Línea Significado
TypeError: Cannot read properties of null Qué ha pasado
at pesoDePrioridad (…:4:20) Dónde ha reventado: fichero, línea 4, columna 20
at esfuerzoDeTarea (…:8:10) Quién la llamó
at informeBacklog (…:14:14) Quién llamó a esa
at Object.<anonymous> (…:19:13) El código de nivel superior

Tres consejos para aprovecharla:

  1. La primera línea at es dónde reventó; la causa suele estar más abajo. Aquí el fallo real no es de pesoDePrioridad, sino de quien le pasó null: es informeBacklog quien no validó los datos.
  2. <anonymous> es una función sin nombre. Otra razón para asignar siempre las expresiones a una const con nombre, como se recomendó en 03-02.
  3. Los números son línea:columna. En el navegador son enlaces directos al código en las DevTools.

La lección Depuración de JavaScript profundiza en esto: puntos de interrupción, inspección de la pila en vivo, mapas de código fuente y console.trace().

  1. Caso práctico: ordenar js/app.js

Este es el fichero js/app.js de Nómada Tareas escrito con el peor orden posible. Funciona a medias, y eso es lo peligroso.

// ✗ js/app.js — MAL ORDENADO
'use strict';

// 1. Se usa la configuración antes de definirla
console.log(`Panel de Nómada Tareas · ${TITULO_APP}`);   // ✗ ReferenceError

const TITULO_APP = 'Taller Nómada';

// 2. Se llama a una expresión antes de asignarla
console.log(formatearEstado('hecha'));                   // ✗ ReferenceError

const formatearEstado = (estado) => {
  if (estado === 'hecha') return 'Hecha';
  return 'Pendiente';
};

// 3. Esto SÍ funciona, y por eso confunde: es una declaración
console.log(pesoDePrioridad('alta'));                    // 3

function pesoDePrioridad(prioridad) {
  return prioridad === 'alta' ? 3 : prioridad === 'media' ? 2 : 1;
}

// 4. var elevada: no da error, da undefined
console.log(`Tareas cargadas: ${totalTareas}`);          // Tareas cargadas: undefined

var totalTareas = 6;

// 5. Función declarada dentro de un bloque
if (totalTareas > 0) {
  function iniciar() { console.log('Arrancando…'); }
}
iniciar();                                               // ✗ TypeError en modo estricto

Cinco problemas en veinticinco líneas. Y la versión corregida, que es la que usarás como plantilla el resto del curso:

// ✓ js/app.js — ORDEN CORRECTO
'use strict';

// ─── 1. Configuración: constantes primero ──────────────────────────
const TITULO_APP = 'Taller Nómada';
const HOY = '2026-09-20';
const LIMITE_SEMANAL = 40;                 // R7

// ─── 2. Datos ──────────────────────────────────────────────────────
const titulos      = [
  'Rediseñar la sala polivalente',
  'Cartelería del taller de serigrafía',
  'Actualizar la web de reservas',
  'Inventario de tintas de serigrafía',
  'Guía de encuadernación para residentes',
  'Presupuesto de la carpintería'
];
const responsables = ['Iván', 'Marta', 'Lucía', 'Marta', 'Iván', 'Iván'];
const prioridades  = ['alta', 'media', 'alta', 'baja', 'media', 'alta'];
const estados      = ['en-curso', 'pendiente', 'pendiente', 'hecha', 'en-curso', 'pendiente'];
const horas        = [12, 6, 14, 3, 8, 5];
const fechasLimite = ['2026-09-30', '2026-10-15', '2026-10-02',
                      '2026-09-12', '2026-11-05', '2026-09-05'];

// ─── 3. Funciones puras del dominio ────────────────────────────────
function pesoDePrioridad(prioridad) {
  if (prioridad === 'alta') return 3;
  if (prioridad === 'media') return 2;
  if (prioridad === 'baja') return 1;
  return 0;
}

function estaVencida(fechaLimite, estado, hoy) {
  return fechaLimite < hoy && estado !== 'hecha';
}

function formatearEstado(estado) {
  if (estado === 'pendiente') return 'Pendiente';
  if (estado === 'en-curso') return 'En curso';
  if (estado === 'hecha') return 'Hecha';
  return 'Desconocido';
}

function describirTarea(indice, hoy) {
  const aviso = estaVencida(fechasLimite[indice], estados[indice], hoy) ? ' ⚠ VENCIDA' : '';
  return `[${indice + 1}] ${titulos[indice]} · ${responsables[indice]} · ` +
         `${formatearEstado(estados[indice])} · ${horas[indice]} h${aviso}`;
}

// ─── 4. Punto de entrada: SIEMPRE al final ─────────────────────────
function iniciar() {
  console.log(`Panel de Nómada Tareas · ${TITULO_APP}`);

  let esfuerzoTotal = 0;
  for (let i = 0; i < titulos.length; i++) {
    console.log(describirTarea(i, HOY));
    esfuerzoTotal += pesoDePrioridad(prioridades[i]) * horas[i];
  }

  console.log(`Esfuerzo ponderado: ${esfuerzoTotal}`);
  console.log(`Límite semanal por persona: ${LIMITE_SEMANAL} h`);
}

iniciar();

Salida:

Panel de Nómada Tareas · Taller Nómada
[1] Rediseñar la sala polivalente · Iván · En curso · 12 h
[2] Cartelería del taller de serigrafía · Marta · Pendiente · 6 h
[3] Actualizar la web de reservas · Lucía · Pendiente · 14 h
[4] Inventario de tintas de serigrafía · Marta · Hecha · 3 h
[5] Guía de encuadernación para residentes · Iván · En curso · 8 h
[6] Presupuesto de la carpintería · Iván · Pendiente · 5 h ⚠ VENCIDA
Esfuerzo ponderado: 124
Límite semanal por persona: 40 h

El orden canónico de un fichero, que conviene adoptar como hábito:

flowchart TD
    A["1 · 'use strict'"] --> B["2 · Constantes de configuración"]
    B --> C["3 · Datos"]
    C --> D["4 · Funciones auxiliares y del dominio"]
    D --> E["5 · Función iniciar() / main()"]
    E --> F["6 · Una única llamada: iniciar()"]

  1. Por qué el estilo moderno hace irrelevante el hoisting

Si comparas las dos versiones del apartado anterior, la conclusión salta a la vista: en la versión ordenada, el hoisting no importa en absoluto. Ninguna línea depende de él. Y esa es exactamente la meta.

Práctica moderna Qué problema de hoisting elimina
const por defecto, let cuando cambie, var nunca Sin elevación a undefined ni contaminación global
Declarar antes de usar, siempre La TDZ nunca se alcanza
Un único punto de entrada iniciar() llamado al final Todo está definido cuando se ejecuta algo
No declarar funciones dentro de bloques Sin diferencias entre modos ni navegadores
'use strict' (o módulos ES, que ya son estrictos) Errores tempranos en lugar de comportamientos raros
ESLint con no-use-before-define El editor avisa antes de ejecutar

Dicho de otro modo: entender el hoisting sirve sobre todo para leer código ajeno y para interpretar errores; no para escribir código que dependa de él. Es conocimiento de diagnóstico, no una técnica.

Errores Comunes y Consejos

1. Creer que var no se eleva porque el console.log da undefined. Se eleva; lo que no se eleva es la asignación.

2. Creer que let y const no se elevan. Sí se elevan; simplemente permanecen sin inicializar hasta su línea. Si no se elevaran, el error diría "is not defined" en lugar de "cannot access before initialization".

3. Usar typeof como red de seguridad con let/const. No protege de la TDZ.

4. Declarar funciones dentro de if o for. Comportamiento distinto según modo y motor. Usa expresiones asignadas a variables externas.

5. Ignorar el orden del stack trace. La línea de arriba es dónde reventó, no necesariamente dónde está el error. Lee toda la traza.

6. Confundir la pila de llamadas con la memoria de un closure. Los contextos de la pila se destruyen al terminar; el entorno capturado por un closure sobrevive porque hay una función que lo referencia.

7. Consejo: si dudas del orden de tu fichero, muévelo todo dentro de iniciar(). Un único punto de entrada llamado al final elimina de golpe casi todos los problemas de orden.

8. Consejo: activa no-use-before-define en tu configuración de ESLint. Convierte estos errores en avisos del editor antes de ejecutar nada (08-02).

Ejercicios

Ejercicio 1 — Predecir la salida

Sin ejecutar el código, indica qué imprime cada línea o qué error lanza, y explica en qué fase se decide.

'use strict';

console.log(a);            // (1)
console.log(f());          // (2)
console.log(g);            // (3)

var a = 1;
function f() { return 'f'; }
let b = 2;
const g = () => 'g';

function prueba() {
  console.log(c);          // (4)
  console.log(d);          // (5)
  var c = 'c';
  let d = 'd';
}
prueba();

Ejercicio 2 — Arreglar el fichero

Este js/app.js tiene cuatro problemas relacionados con el orden y el hoisting. Identifícalos, explica por qué fallan y reescribe el fichero completo siguiendo el orden canónico.

'use strict';

iniciar();

var contador = 0;

function iniciar() {
  console.log(`Backlog de ${EMPRESA}`);
  for (var i = 0; i < 3; i++) {
    registrar(i);
  }
  console.log(`Registros: ${contador}`);
  console.log(margen(45));
}

const EMPRESA = 'Taller Nómada';

if (true) {
  function registrar(n) { contador++; console.log(`Registro ${n}`); }
}

const margen = (horas) => LIMITE - horas;
var LIMITE = 40;

Ejercicio 3 — Profundidad de la pila

Escribe una función sumarHorasRecursiva(horas, indice = 0) que sume el array de horas del backlog llamándose a sí misma. Después:

  1. Comprueba que devuelve 48 con el backlog.
  2. Genera un array de 50 000 horas y llámala. Explica qué ocurre y por qué.
  3. Reescríbela con un bucle y explica por qué esa versión no tiene el problema.

Soluciones

Ejercicio 1

Resultado Fase donde se decide
(1) undefined Creación: var a se registró e inicializó a undefined
(2) 'f' Creación: la declaración de función se elevó completa
(3) ReferenceError: Cannot access 'g' before initialization Creación: const g registrada pero en TDZ
(4) undefined Creación del contexto de prueba: var c a undefined
(5) ReferenceError let d está en TDZ dentro de ese contexto

En la práctica el programa se detiene en (3) y las líneas siguientes no llegan a ejecutarse. Para verlas, hay que eliminar o comentar esa línea, y entonces se detendría en (5).

Ejercicio 2

Los cuatro problemas:

# Problema Consecuencia
1 iniciar() se llama en la primera línea Dentro se lee EMPRESA, que aún está en TDZ → ReferenceError
2 registrar se declara dentro de un if en modo estricto Ámbito de bloque: fuera es undefinedTypeError
3 margen es una flecha usada antes de asignarse ReferenceError
4 LIMITE es var, y además se lee dentro de margen Aunque el orden se arreglara, var a nivel global contamina; debe ser const

Versión corregida:

'use strict';

// 1. Configuración
const EMPRESA = 'Taller Nómada';
const LIMITE = 40;

// 2. Estado del módulo
let contador = 0;

// 3. Funciones
function registrar(n) {
  contador = contador + 1;
  console.log(`Registro ${n}`);
}

const margen = (horas) => LIMITE - horas;

// 4. Punto de entrada
function iniciar() {
  console.log(`Backlog de ${EMPRESA}`);
  for (let i = 0; i < 3; i++) {
    registrar(i);
  }
  console.log(`Registros: ${contador}`);
  console.log(margen(45));
}

// 5. Única llamada, al final
iniciar();

Salida:

Backlog de Taller Nómada
Registro 0
Registro 1
Registro 2
Registros: 3
-5

Comentario: contador es let porque cambia, EMPRESA y LIMITE son const porque no, y let i sustituye a var i en el bucle. El -5 revela además un problema de negocio: 45 h superan el límite de 40 (R7), algo que una validación debería impedir antes de llegar aquí.

Ejercicio 3

function sumarHorasRecursiva(horas, indice = 0) {
  if (indice >= horas.length) return 0;                    // caso base
  return horas[indice] + sumarHorasRecursiva(horas, indice + 1);
}

// 1)
console.log(sumarHorasRecursiva([12, 6, 14, 3, 8, 5]));    // 48

// 2)
const muchas = [];
for (let i = 0; i < 50000; i++) muchas.push(1);

try {
  console.log(sumarHorasRecursiva(muchas));
} catch (error) {
  console.error(`${error.name}: ${error.message}`);
}
// RangeError: Maximum call stack size exceeded

// 3)
function sumarHorasIterativa(horas) {
  let total = 0;
  for (const h of horas) total += h;
  return total;
}

console.log(sumarHorasIterativa(muchas));   // 50000

Explicación: la versión recursiva apila 50 001 contextos, porque ninguna llamada puede terminar hasta que termine la siguiente (la suma se hace al volver). El motor agota el espacio de la pila y lanza RangeError. La versión iterativa usa un solo contexto y una variable acumuladora, así que su consumo de pila es constante independientemente del tamaño del array. Este es exactamente el compromiso que analizarás en Recursividad.

Conclusión

Ya sabes qué hace el motor antes de ejecutar tu código. Cada ámbito pasa por una fase de creación, donde se registran las declaraciones —las funciones completas y listas, las var como undefined, y let/const sin inicializar—, y una fase de ejecución, donde se asignan los valores línea a línea. Ese es todo el misterio del hoisting: nada se mueve, simplemente el motor ya conocía los nombres. La zona muerta temporal de let y const no es un obstáculo sino una protección: convierte en error inmediato lo que con var era un undefined silencioso, y por eso el mensaje distingue entre "is not defined" y "cannot access before initialization".

Has visto también la maquinaria que sostiene todo lo anterior: el contexto de ejecución, con su entorno de variables, su referencia al ámbito exterior —la cadena que explicaba los closures de 03-04— y su this; el contexto global, uno por script, donde var contamina el objeto global y let/const no; y la pila de llamadas, de un solo hilo y tamaño limitado, cuya saturación produce el RangeError: Maximum call stack size exceeded. Y sabes leer un stack trace: qué falló arriba, quién llamó a quién debajo, y que la causa raíz casi nunca está en la primera línea.

Lo más importante es la conclusión práctica: con const por defecto, las funciones declaradas antes de usarse, ningún var, ninguna función dentro de bloques y un único iniciar() llamado al final, el hoisting deja de importar. El js/app.js ordenado de esta lección es la plantilla que seguirás el resto del curso.

Con esto cierras la parte de "cómo funcionan las funciones por dentro". Lo que viene ahora es sacarles partido. Ya has rozado la idea varias veces —listarSegun(filtro) recibía una función, crearFiltroPorResponsable devolvía una— sin ponerle nombre. Ese nombre es funciones de orden superior, y con ellas construirás en Funciones de Orden Superior tus propias versiones de map, filter y reduce, un sistema de composición y un motor de informes configurable para Taller Nómada.

Curso de JavaScript: De Principiante a Avanzado

Módulo 1: Introducción a JavaScript

Módulo 2: Estructuras de Control

Módulo 3: Funciones

Módulo 4: Objetos y Arrays

Módulo 5: Objetos y Funciones Avanzadas

Módulo 6: El Modelo de Objetos del Documento (DOM)

Módulo 7: APIs del Navegador y Temas Avanzados

Módulo 8: Pruebas y Depuración

Módulo 9: Rendimiento y Optimización

Módulo 10: Frameworks y Librerías de JavaScript

Módulo 11: Proyecto Final

© Copyright 2026. Todos los derechos reservados