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
- El motor no ejecuta a la primera pasada
- Fase de creación y fase de ejecución
- Hoisting de declaraciones de función
- Hoisting de
var letyconst: la zona muerta temporal- Tabla comparativa completa
- Trazas comentadas paso a paso
- El contexto de ejecución global
- Los contextos de función
- La pila de llamadas
RangeError: Maximum call stack size exceeded- Leer un stack trace real
- Caso práctico: ordenar
js/app.js - Por qué el estilo moderno hace irrelevante el hoisting
- Errores Comunes y Consejos
- Ejercicios
- Conclusión
- 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.
- 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.
- 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 existeLas 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 estrictoConclusió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.
- Hoisting de
var
varUna 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.
let y const: la zona muerta temporal
let y const: la zona muerta temporallet 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;
- Tabla comparativa completa
function f(){} |
var x |
let x |
const x |
|
|---|---|---|---|---|
| ¿Se eleva el nombre? | Sí | Sí | Sí | Sí |
| ¿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 | Sí | Sí | No | No |
| Reasignable | Sí | Sí | Sí | No |
| Inicialización obligatoria | — | No | No | Sí |
Se añade a globalThis (nivel superior) |
Sí | Sí | No | No |
- 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-20Salida completa:
La moraleja del ejercicio no es memorizar la tabla, sino esto: cualquier fichero que dependa de estas sutilezas está mal ordenado.
- 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); // undefinedPor eso var a nivel global es peligrosa: contamina el objeto global y puede pisar propiedades existentes. let y const no lo hacen.
- 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 — OKLas 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.
- 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]
)); // 124sequenceDiagram
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 → informeBacklog → esfuerzoDeTarea → pesoDePrioridad. Y solo cuatro: el bucle no acumula contextos, porque cada llamada se cierra antes de la siguiente.
Dos propiedades importantes de la pila:
- 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.
- Tiene un tamaño limitado. Y de ahí sale el error del apartado siguiente.
RangeError: Maximum call stack size exceeded
RangeError: Maximum call stack size exceededSi 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 exceededEl 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.
- 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:
- La primera línea
ates dónde reventó; la causa suele estar más abajo. Aquí el fallo real no es depesoDePrioridad, sino de quien le pasónull: esinformeBacklogquien no validó los datos. <anonymous>es una función sin nombre. Otra razón para asignar siempre las expresiones a unaconstcon nombre, como se recomendó en 03-02.- 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().
- Caso práctico: ordenar
js/app.js
js/app.jsEste 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 estrictoCinco 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()"]
- 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:
- Comprueba que devuelve 48 con el backlog.
- Genera un array de 50 000 horas y llámala. Explica qué ocurre y por qué.
- Reescríbela con un bucle y explica por qué esa versión no tiene el problema.
Soluciones
Ejercicio 1
| Nº | 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 undefined → TypeError |
| 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:
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)); // 50000Explicació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
- ¿Qué es JavaScript?
- Configuración de tu Entorno de Desarrollo
- Tu Primer Programa en JavaScript
- Sintaxis y Conceptos Básicos de JavaScript
- Variables y Tipos de Datos
- Operadores Básicos
- Conversión de Tipos y Comparaciones
- El Proyecto del Curso: Nómada Tareas
Módulo 2: Estructuras de Control
- Sentencias Condicionales
- Bucles: for, while, do-while
- Sentencias Switch
- Control del Flujo: break, continue y Bucles Anidados
- Manejo de Errores con try-catch
Módulo 3: Funciones
- Definición y Llamada de Funciones
- Expresiones de Función y Funciones Flecha
- Parámetros y Valores de Retorno
- Ámbito y Closures
- Hoisting y el Contexto de Ejecución
- Funciones de Orden Superior
- Recursividad
Módulo 4: Objetos y Arrays
- Introducción a los Objetos
- Métodos de Objeto y la Palabra Clave
this - Arrays: Conceptos Básicos y Métodos
- Iteración sobre Arrays
- Buscar, Ordenar y Agregar Datos: find, sort y reduce
- Desestructuración de Arrays
- Desestructuración de Objetos, Spread y Rest
- JSON y Copias de Objetos
Módulo 5: Objetos y Funciones Avanzadas
- Prototipos y Herencia
- Clases y Programación Orientada a Objetos
- Encapsulación: Getters, Setters y Campos Privados
- Módulos e Importación/Exportación
- JavaScript Asíncrono: Callbacks
- Promesas y Async/Await
- El Bucle de Eventos y la Cola de Microtareas
- Iteradores y Generadores
Módulo 6: El Modelo de Objetos del Documento (DOM)
- Introducción al DOM
- Selección y Manipulación de Elementos del DOM
- Manejo de Eventos
- Propagación, Delegación y Eventos Personalizados
- Creación y Eliminación de Elementos del DOM
- Renderizado de Listas y Plantillas HTML
- Manejo y Validación de Formularios
Módulo 7: APIs del Navegador y Temas Avanzados
- Almacenamiento Local y de Sesión
- Fetch API y AJAX
- Peticiones Robustas: Errores, Timeouts y AbortController
- WebSockets
- Service Workers y Aplicaciones Web Progresivas (PWAs)
- APIs del Navegador Esenciales
- Introducción a WebAssembly
Módulo 8: Pruebas y Depuración
- Depuración de JavaScript
- Calidad de Código: ESLint, Prettier y Convenciones
- Pruebas Unitarias con Jest
- Dobles de Prueba: Mocks, Stubs y Spies
- Pruebas de Integración
- Pruebas de Extremo a Extremo con Cypress
Módulo 9: Rendimiento y Optimización
- Medir Antes de Optimizar: DevTools y Web Vitals
- Optimización del Rendimiento de JavaScript
- Gestión de Memoria
- Manipulación Eficiente del DOM
- Carga Perezosa y División de Código
Módulo 10: Frameworks y Librerías de JavaScript
- Por Qué Existen los Frameworks
- Introducción a React
- Gestión de Estado con Redux
- Conceptos Básicos de Vue.js
- Conceptos Básicos de Angular
- Elegir el Framework Adecuado
