La lección anterior terminó con una pregunta abierta: cuando crearTarea() hace ultimoId = ultimoId + 1, está tocando una variable que vive fuera de ella. ¿Por qué puede verla? ¿Y se podría tener un contador de identificadores que nadie más pudiera tocar por accidente, sin dejarlo suelto en el fichero? Responder a eso exige entender dos conceptos que están entre los más importantes de JavaScript: el ámbito (dónde vive y desde dónde se ve cada variable) y los closures (la capacidad de una función de recordar el entorno donde nació). Con ellos podrás crear estado privado, fábricas de funciones configuradas y cachés de resultados, tres herramientas que usarás en Nómada Tareas hasta el final del curso.
Contenido
- Qué es el ámbito
- Ámbito global
- Ámbito de función
- Ámbito de bloque:
letyconstfrente avar - La cadena de ámbitos, paso a paso
- Sombreado de variables (shadowing)
- Qué es un closure
- El closure explicado con un ejemplo mínimo
- El clásico bucle con
varfrente alet - Patrón 1: generador de identificadores
- Patrón 2: fábrica de funciones configuradas
- Patrón 3: estado privado con el patrón módulo
- Patrón 4: memoización sencilla
- El coste en memoria
- Errores Comunes y Consejos
- Ejercicios
- Conclusión
- Qué es el ámbito
El ámbito (scope) de una variable es la región del código desde la que se puede usar su nombre. No es un detalle académico: es lo que determina si una línea funciona o lanza ReferenceError.
JavaScript tiene tres tipos de ámbito:
| Ámbito | Se crea con | Vive mientras | Lo declaran |
|---|---|---|---|
| Global | El fichero o el <script> |
Dure el programa | const/let/var fuera de todo |
| De función | Cada llamada a una función | Dure la llamada | Parámetros y cualquier declaración interna |
| De bloque | Cada { } (if, for, bloque suelto) |
Dure el bloque | Solo const y let (no var) |
Y una regla que gobierna todo lo demás:
Desde dentro se ve hacia fuera; desde fuera no se ve hacia dentro.
- Ámbito global
Todo lo declarado fuera de cualquier función y de cualquier bloque es global: visible desde cualquier punto del fichero.
'use strict';
const HOY = '2026-09-20'; // global: constante de configuración del proyecto
const LIMITE_SEMANAL = 40; // global: regla R7
function margenDe(horasAbiertas) {
return LIMITE_SEMANAL - horasAbiertas; // ✓ ve la global
}
console.log(margenDe(25)); // 15Las globales son útiles para constantes de configuración y peligrosas para todo lo demás:
| Riesgo | Explicación |
|---|---|
| Colisiones de nombre | Dos ficheros que declaran tareas se pisan |
| Acoplamiento invisible | Una función que lee una global no dice en su firma de qué depende |
| Imposible de probar | No puedes ejecutarla con otros valores sin modificar la global |
| Mutación desde cualquier sitio | Cualquier línea del programa puede cambiarla |
Regla práctica: globales solo en const y solo para configuración. Todo lo variable, dentro de funciones o protegido por un closure, como verás en el apartado 12.
- Ámbito de función
Cada llamada a una función crea un ámbito nuevo. Los parámetros y las variables declaradas en su cuerpo existen ahí y desaparecen al terminar.
function resumirResponsable(nombre, horas) {
const limite = 40; // local
const margen = limite - horas; // local
return `${nombre}: margen de ${margen} h`;
}
console.log(resumirResponsable('Iván', 25)); // Iván: margen de 15 h
console.log(limite); // ✗ ReferenceError: limite is not definedY algo importante: dos llamadas no comparten nada.
function contarDesdeCero() {
let contador = 0; // nace de nuevo en cada llamada
contador++;
return contador;
}
console.log(contarDesdeCero()); // 1
console.log(contarDesdeCero()); // 1 ← no recuerda nadaEsa amnesia es lo que los closures del apartado 7 permiten romper cuando conviene.
- Ámbito de bloque:
let y const frente a var
let y const frente a varUn bloque es cualquier par de llaves: el cuerpo de un if, de un for, de un while o unas llaves sueltas. let y const se quedan dentro del bloque; var no.
function analizarTarea(estado, horas) {
if (estado === 'hecha') {
const mensaje = 'Tarea completada'; // solo dentro del if
var registro = 'hecha'; // ¡se escapa a toda la función!
}
console.log(registro); // 'hecha' ← var ignora el bloque
console.log(mensaje); // ✗ ReferenceError
}La comparación completa de las tres formas de declarar:
var |
let |
const |
|
|---|---|---|---|
| Ámbito | Función | Bloque | Bloque |
| Se puede reasignar | Sí | Sí | No |
| Se puede redeclarar en el mismo ámbito | Sí (peligroso) | No | No |
| Antes de la línea de declaración | undefined |
Error (TDZ) | Error (TDZ) |
Se añade a globalThis si es global |
Sí | No | No |
| Recomendación | No usar | Cuando cambie | Por defecto |
Ese "se puede redeclarar" de var es especialmente traicionero:
var responsable = 'Iván';
var responsable = 'Marta'; // ✓ sin error: has perdido el valor anterior sin enterarte
let revisor = 'Lucía';
let revisor = 'Marta'; // ✗ SyntaxError: Identifier 'revisor' has already been declaredEn este curso no usarás var. Aparece aquí solo porque lo verás en código antiguo y porque su comportamiento explica el ejercicio clásico del apartado 9.
- La cadena de ámbitos, paso a paso
Cuando el motor encuentra un nombre, lo busca en el ámbito actual. Si no está, sube al ámbito que lo contiene. Y así sucesivamente hasta el global. Si tampoco está allí: ReferenceError.
'use strict';
const HOY = '2026-09-20'; // ── Ámbito global
function generarInforme(responsable) { // ── Ámbito de generarInforme
const cabecera = `Informe de ${responsable}`;
function detallar(horas) { // ── Ámbito de detallar
const linea = `${horas} h`;
return `${cabecera} · ${linea} · al ${HOY}`;
// ↑ ámbito padre ↑ local ↑ global
}
return detallar(25);
}
console.log(generarInforme('Iván'));
// Informe de Iván · 25 h · al 2026-09-20Así busca el motor cada nombre dentro de detallar:
flowchart TD
A["Ámbito de detallar<br/>linea, horas"] -->|no encuentra 'cabecera'| B["Ámbito de generarInforme<br/>cabecera, responsable"]
B -->|no encuentra 'HOY'| C["Ámbito global<br/>HOY, generarInforme"]
C -->|no encuentra nada| D["ReferenceError"]
Tres consecuencias que conviene fijar:
- La búsqueda es unidireccional: hacia arriba. Desde
generarInformeno se puede verlinea. - Para en la primera coincidencia. Si
detallardeclarara su propiocabecera, la búsqueda se detendría ahí (apartado 6). - La cadena se decide al escribir el código, no al ejecutarlo. Esto se llama ámbito léxico, y es la clave para entender los closures: lo que importa es dónde está escrita la función, no desde dónde se la llama.
- Sombreado de variables (shadowing)
Cuando un ámbito interno declara una variable con el mismo nombre que una externa, la interna tapa a la externa dentro de su región.
const estado = 'global';
function inspeccionar() {
const estado = 'de función';
if (true) {
const estado = 'de bloque';
console.log(estado); // de bloque
}
console.log(estado); // de función
}
inspeccionar();
console.log(estado); // globalEl sombreado es legal y a veces útil (un parámetro llamado tarea dentro de una función que trabaja con tareas), pero también es una fuente de confusión. Un caso real que muerde:
const LIMITE_SEMANAL = 40;
function comprobarCarga(horas, LIMITE_SEMANAL) { // ✗ el parámetro sombrea la constante
return horas > LIMITE_SEMANAL;
}
console.log(comprobarCarga(45)); // false ← LIMITE_SEMANAL es undefined dentroQuien lee el cuerpo asume que LIMITE_SEMANAL vale 40, pero dentro vale undefined porque no se pasó el segundo argumento, y 45 > undefined es false. Reglas para evitarlo:
- No reutilices nombres de constantes globales como parámetros.
- Si el linter avisa de
no-shadow, hazle caso (08-02). - Cuando el sombreado sea deliberado, que se note:
tareafuera ytareaOriginaldentro, por ejemplo.
- Qué es un closure
Un closure (o clausura) es una función junto con el entorno de variables en el que fue creada. Dicho de otro modo:
Cuando una función se define dentro de otra, recuerda las variables de la función exterior, incluso después de que esa función exterior haya terminado.
La parte sorprendente es la última. Ya sabes que las variables de una función desaparecen al terminar la llamada. Pero si una función interna sigue usándolas, no desaparecen: el motor las mantiene vivas todo el tiempo que haga falta.
flowchart LR
subgraph E["Entorno capturado (sobrevive)"]
V["contador = 0"]
end
F["función interna<br/>devuelta al exterior"] --> V
C["quien la llama<br/>desde fuera"] --> F
- El closure explicado con un ejemplo mínimo
Empecemos por el caso más pequeño posible:
function crearSaludo(nombre) {
const saludo = `Hola, ${nombre}`; // vive en el ámbito de crearSaludo
return function () { // función interna
return saludo; // usa una variable del ámbito padre
};
}
const saludarAMarta = crearSaludo('Marta');
// crearSaludo YA ha terminado... y sin embargo:
console.log(saludarAMarta()); // Hola, MartaLee bien la secuencia:
- Se llama a
crearSaludo('Marta')y se crea un ámbito connombreysaludo. - Se crea la función interna, que referencia a
saludo. crearSaludotermina y devuelve esa función.- Normalmente
saludodesaparecería. Pero como la función devuelta lo necesita, el motor conserva el entorno. - Al llamar a
saludarAMarta(), la función encuentra susaludointacto.
Y cada llamada a crearSaludo crea un entorno independiente:
const saludarAIvan = crearSaludo('Iván');
const saludarALucia = crearSaludo('Lucía');
console.log(saludarAIvan()); // Hola, Iván
console.log(saludarALucia()); // Hola, LucíaDos funciones idénticas en su código, con memorias distintas. Ese es el mecanismo completo. Todo lo que viene después son aplicaciones de esta misma idea.
- El clásico bucle con
var frente a let
var frente a letEste ejercicio aparece en todas las entrevistas de trabajo y, más importante, explica un error real que verás en el Módulo 6 al asignar manejadores de eventos en un bucle.
const titulos = ['Rediseñar la sala', 'Cartelería', 'Web de reservas'];
const acciones = [];
for (var i = 0; i < titulos.length; i++) {
acciones.push(function () {
console.log(`${i}: ${titulos[i]}`);
});
}
acciones[0](); // 3: undefined
acciones[1](); // 3: undefined
acciones[2](); // 3: undefined¿Por qué? Porque var i tiene ámbito de función, no de bloque: hay una sola i para todo el bucle. Las tres funciones capturan la misma variable, y cuando se ejecutan (después del bucle) esa i ya vale 3.
Con let, cada iteración crea su propia i:
for (let i = 0; i < titulos.length; i++) {
acciones.push(function () {
console.log(`${i}: ${titulos[i]}`);
});
}
acciones[0](); // 0: Rediseñar la sala
acciones[1](); // 1: Cartelería
acciones[2](); // 2: Web de reservasflowchart TD
subgraph VAR["Con var: una sola variable"]
A1["fn 0"] --> I["i = 3"]
A2["fn 1"] --> I
A3["fn 2"] --> I
end
subgraph LET["Con let: una por iteración"]
B1["fn 0"] --> J1["i = 0"]
B2["fn 1"] --> J2["i = 1"]
B3["fn 2"] --> J3["i = 2"]
end
Antes de let, la solución era envolver cada iteración en una IIFE —el patrón que viste en Expresiones de Función y Funciones Flecha— precisamente para crear un ámbito nuevo por vuelta:
for (var i = 0; i < titulos.length; i++) {
(function (indice) {
acciones.push(function () { console.log(`${indice}: ${titulos[indice]}`); });
})(i);
}Hoy basta con escribir let. Pero ahora sabes por qué.
- Patrón 1: generador de identificadores
Volvemos al cabo suelto de la lección anterior. ultimoId estaba suelta en el fichero, visible y modificable por cualquiera. Con un closure, deja de existir para el resto del programa.
'use strict';
/**
* Devuelve una función que produce identificadores correlativos (R1).
* El contador queda encerrado: nadie fuera puede leerlo ni cambiarlo.
*/
function crearGeneradorDeIds(inicial = 0) {
let ultimoId = inicial;
return function siguienteId() {
ultimoId = ultimoId + 1;
return ultimoId;
};
}
const siguienteId = crearGeneradorDeIds(6); // el backlog ya tiene 6 tareas
console.log(siguienteId()); // 7
console.log(siguienteId()); // 8
console.log(siguienteId()); // 9
console.log(typeof ultimoId); // 'undefined' ← protegidaCompara las dos versiones:
Variable global ultimoId |
Closure crearGeneradorDeIds |
|
|---|---|---|
| Quién puede modificarla | Cualquier línea del programa | Solo la función devuelta |
| Riesgo de que otro fichero la pise | Alto | Ninguno |
| Varios generadores independientes | Imposible | crearGeneradorDeIds() otra vez |
| Valor inicial configurable | Editando el fichero | Por parámetro |
Ese último punto es muy útil en las pruebas: en el Módulo 8 podrás crear un generador que empiece en 0 para cada test, sin que un test contamine al siguiente.
const idsDePrueba = crearGeneradorDeIds();
console.log(idsDePrueba()); // 1 ← independiente del generador de producción
console.log(siguienteId()); // 10 ← el otro sigue por su cuenta
- Patrón 2: fábrica de funciones configuradas
Una fábrica es una función que devuelve otra función ya configurada con unos parámetros fijos. Evita repetir esos parámetros en cada llamada.
const responsables = ['Iván', 'Marta', 'Lucía', 'Marta', 'Iván', 'Iván'];
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 horas = [12, 6, 14, 3, 8, 5];
const estados = ['en-curso', 'pendiente', 'pendiente', 'hecha', 'en-curso', 'pendiente'];
/**
* Devuelve una función que dice si la tarea del índice i pertenece a `nombre`.
* `nombre` queda capturado en el closure.
*/
function crearFiltroPorResponsable(nombre) {
return function (indice) {
return responsables[indice] === nombre;
};
}
const esDeIvan = crearFiltroPorResponsable('Iván');
const esDeMarta = crearFiltroPorResponsable('Marta');
console.log(esDeIvan(0)); // true
console.log(esDeIvan(1)); // false
console.log(esDeMarta(1)); // trueY así se usa para recorrer el backlog con criterios intercambiables:
function listarSegun(filtro) {
const encontradas = [];
for (let i = 0; i < titulos.length; i++) {
if (filtro(i)) encontradas.push(titulos[i]);
}
return encontradas;
}
console.log(listarSegun(esDeIvan));
// [ 'Rediseñar la sala polivalente',
// 'Guía de encuadernación para residentes',
// 'Presupuesto de la carpintería' ]
// Y las fábricas se pueden combinar
function crearFiltroPorEstado(estado) {
return (indice) => estados[indice] === estado;
}
function crearFiltroCombinado(filtroA, filtroB) {
return (indice) => filtroA(indice) && filtroB(indice);
}
const pendientesDeIvan = crearFiltroCombinado(esDeIvan, crearFiltroPorEstado('pendiente'));
console.log(listarSegun(pendientesDeIvan));
// [ 'Presupuesto de la carpintería' ]Fíjate en lo que acaba de ocurrir: listarSegun no sabe nada de responsables ni de estados; solo sabe llamar a una función que le dicen. Eso es una función de orden superior, y es el tema completo de Funciones de Orden Superior.
- Patrón 3: estado privado con el patrón módulo
El patrón módulo combina un closure con un objeto de retorno: las variables quedan privadas y solo se expone un conjunto controlado de funciones.
'use strict';
function crearAlmacenDeTareas(idInicial = 0) {
// ── Estado privado: inaccesible desde fuera ──────────────────
const tareas = [];
let ultimoId = idInicial;
// ── Función auxiliar privada ─────────────────────────────────
function buscarIndice(id) {
for (let i = 0; i < tareas.length; i++) {
if (tareas[i].id === id) return i;
}
return -1;
}
// ── Interfaz pública ─────────────────────────────────────────
return {
anyadir: function (titulo, responsable, horasEstimadas) {
ultimoId = ultimoId + 1;
tareas.push({
id: ultimoId,
titulo: titulo,
responsable: responsable ?? null,
estado: 'pendiente',
horasEstimadas: horasEstimadas
});
return ultimoId;
},
cambiarEstado: function (id, nuevoEstado) {
const i = buscarIndice(id);
if (i === -1) throw new Error(`No existe la tarea ${id}.`);
tareas[i].estado = nuevoEstado;
return true;
},
contar: function () {
return tareas.length;
},
horasAbiertas: function () {
let total = 0;
for (const t of tareas) {
if (t.estado !== 'hecha') total += t.horasEstimadas;
}
return total;
}
};
}
const almacen = crearAlmacenDeTareas(6);
console.log(almacen.anyadir('Renovar el torno', 'Lucía', 10)); // 7
console.log(almacen.anyadir('Comprar tintas', null, 2)); // 8
console.log(almacen.contar()); // 2
console.log(almacen.horasAbiertas()); // 12
almacen.cambiarEstado(7, 'hecha');
console.log(almacen.horasAbiertas()); // 2
console.log(almacen.tareas); // undefined ← protegido
console.log(typeof buscarIndice); // 'undefined' ← privadaLas garantías que ofrece esta estructura son reales, no de estilo:
- Nadie puede hacer
almacen.tareas.push(basura), porquetareasno está expuesto. ultimoIdno se puede alterar, así que la R1 (identificadores únicos y correlativos) está garantizada por construcción.buscarIndicees un detalle interno: se puede reescribir sin romper a nadie.
En el Módulo 5 verás dos evoluciones de esta idea: las clases con campos privados y los módulos ES, que consiguen lo mismo a nivel de fichero. El patrón módulo con closures es el antecesor de ambos y sigue siendo perfectamente válido.
- Patrón 4: memoización sencilla
Memoizar es guardar el resultado de una función para no volver a calcularlo con los mismos argumentos. El almacén de resultados vive en un closure.
/**
* Cuenta las tareas de un responsable, guardando en caché cada resultado.
* Solo es correcto porque contarTareas es una función PURA (ver 03-03).
*/
function crearContadorMemoizado(responsables) {
const cache = {}; // privado: la memoria de la función
let calculos = 0;
let aciertos = 0;
const contar = function (nombre) {
if (nombre in cache) {
aciertos++;
return cache[nombre];
}
calculos++;
let total = 0;
for (const r of responsables) {
if (r === nombre) total++;
}
cache[nombre] = total;
return total;
};
contar.estadisticas = () => `${calculos} cálculos, ${aciertos} aciertos de caché`;
return contar;
}
const contarTareasDe = crearContadorMemoizado(responsables);
console.log(contarTareasDe('Iván')); // 3 (calculado)
console.log(contarTareasDe('Iván')); // 3 (de caché)
console.log(contarTareasDe('Marta')); // 2 (calculado)
console.log(contarTareasDe('Iván')); // 3 (de caché)
console.log(contarTareasDe.estadisticas()); // 2 cálculos, 2 aciertos de cachéDos avisos importantes sobre la memoización:
- Solo es válida con funciones puras. Si el resultado depende de algo que cambia (el reloj, una variable externa, el contenido del backlog), la caché devolverá datos obsoletos. Por eso 03-03 insistía tanto en la pureza.
- La caché crece. Cada argumento nuevo añade una entrada que no se libera nunca mientras la función viva.
En Recursividad aplicarás exactamente esta técnica a Fibonacci, donde la diferencia de rendimiento es espectacular.
- El coste en memoria
Un closure mantiene vivo todo el entorno de la función exterior mientras la función interna exista. Eso es potente y tiene precio:
function crearFormateadorPesado() {
const backlogCompleto = cargarMilesDeTareas(); // imagina 50 MB de datos
const separador = ' · ';
return function (titulo, horas) {
return titulo + separador + horas; // solo usa `separador`
};
}
const formatear = crearFormateadorPesado();
// `backlogCompleto` puede seguir en memoria mientras `formatear` existaLos motores modernos optimizan bastante bien este caso y suelen liberar lo que la función interna demostradamente no usa, pero no conviene depender de ello. Buenas costumbres:
| Práctica | Motivo |
|---|---|
| Capturar solo lo necesario (extraer el dato antes de cerrar sobre él) | Reduce lo que queda retenido |
Poner a null las referencias grandes cuando ya no hagan falta |
Permite liberarlas |
| Limitar el tamaño de las cachés de memoización | Evita crecimiento sin fin |
| No crear closures dentro de bucles muy largos si no hace falta | Cada uno es un entorno vivo |
Esto conecta directamente con las fugas de memoria de Gestión de Memoria, donde verás cómo detectarlas con las DevTools. Por ahora basta con la idea: un closure no es gratis, pero en el 99 % de los casos su coste es irrelevante comparado con lo que aporta.
Errores Comunes y Consejos
1. Creer que un closure copia el valor. No lo copia: mantiene una referencia viva a la variable.
function crearEtiquetador() {
let prefijo = 'TN';
const etiquetar = (id) => `${prefijo}-${id}`;
prefijo = 'NOMADA'; // se cambia DESPUÉS de crear la función
return etiquetar;
}
console.log(crearEtiquetador()(7)); // NOMADA-7 ← ve el valor actual, no el de entonces2. Compartir estado sin darse cuenta. Dos funciones creadas en la misma llamada comparten entorno:
function crearContadorDoble() {
let n = 0;
return { subir: () => ++n, leer: () => n };
}
const c = crearContadorDoble();
c.subir(); c.subir();
console.log(c.leer()); // 2 ← comparten la misma `n`, que es lo que queremos aquíEs deseable cuando lo buscas y un error cuando no.
3. Usar var en un bucle que crea funciones. El caso del apartado 9. Con let desaparece.
4. Sombrear una constante global con un parámetro. Da resultados false o undefined sin ningún error.
5. Memoizar una función impura. Devolverá datos caducados. Antes de memoizar, comprueba que la función es pura.
6. Pensar que el ámbito depende de quién llama. No: depende de dónde está escrita la función (ámbito léxico). Este es el punto que más cuesta y el que hace predecibles los closures.
const etiqueta = 'global';
function mostrar() {
console.log(etiqueta); // siempre 'global'
}
function llamar() {
const etiqueta = 'local'; // NO afecta a mostrar()
mostrar();
}
llamar(); // global7. Consejo: si una función necesita recordar algo entre llamadas, piensa en un closure antes que en una variable global. Es la diferencia entre estado controlado y estado suelto.
8. Consejo: nombra las fábricas con el prefijo crear…. crearGeneradorDeIds, crearFiltroPorResponsable, crearAlmacenDeTareas. El nombre avisa de que lo que devuelve no es un dato, sino una herramienta.
Ejercicios
Ejercicio 1 — Predecir la salida
Sin ejecutar el código, escribe qué imprime cada console.log y explica por qué.
const etiqueta = 'A';
function externa() {
const etiqueta = 'B';
function interna() {
console.log(etiqueta); // (1)
}
if (true) {
const etiqueta = 'C';
console.log(etiqueta); // (2)
interna(); // (3)
}
interna(); // (4)
}
externa();
console.log(etiqueta); // (5)
function contadorRoto() {
var n = 0;
const fns = [];
for (var i = 0; i < 3; i++) {
fns.push(() => i + n);
}
return fns;
}
const fns = contadorRoto();
console.log(fns[0](), fns[1](), fns[2]()); // (6)Ejercicio 2 — crearContadorDeEstado
Escribe una fábrica crearContadorDeEstado(estados) que reciba el array de estados del backlog y devuelva un objeto con tres funciones: registrar(estado) (suma uno al recuento de ese estado), recuento(estado) (devuelve cuántas veces se ha registrado) y resumen() (devuelve una cadena con los tres estados y sus recuentos). El recuento debe ser privado. Inicialízalo recorriendo el array recibido.
Ejercicio 3 — Memoizar calcularEsfuerzo
Escribe memoizar(funcion), una fábrica genérica que reciba una función de un parámetro de tipo cadena o número y devuelva una versión memoizada. Aplícala a una función calcularEsfuerzo(prioridad) que sea deliberadamente costosa (haz un bucle de un millón de vueltas dentro) y mide con Date.now() la diferencia entre la primera y la segunda llamada.
Soluciones
Ejercicio 1
| Nº | Salida | Explicación |
|---|---|---|
| (2) | C |
El const del bloque if sombrea al de la función |
| (3) | B |
Ámbito léxico: interna está escrita dentro de externa, así que ve la etiqueta de externa. Que se la llame desde el bloque es irrelevante |
| (4) | B |
Misma razón |
| (1) | — | Es la línea que produce (3) y (4); no se ejecuta por sí sola |
| (5) | A |
La global nunca fue tocada |
| (6) | 3 3 3 |
var i es única para toda la función; al ejecutar las flechas, i ya vale 3 y n vale 0 |
Orden real de impresión: C, B, B, A, 3 3 3.
Para arreglar (6) basta con cambiar var i por let i, y entonces daría 0 1 2.
Ejercicio 2
function crearContadorDeEstado(estadosIniciales = []) {
// Estado privado
const recuentos = { pendiente: 0, 'en-curso': 0, hecha: 0 };
function sumar(estado) {
if (!(estado in recuentos)) {
throw new Error(`Estado desconocido: ${estado}`);
}
recuentos[estado] = recuentos[estado] + 1;
}
// Inicialización a partir del array recibido
for (const e of estadosIniciales) {
sumar(e);
}
return {
registrar: (estado) => { sumar(estado); },
recuento: (estado) => recuentos[estado] ?? 0,
resumen: () =>
`pendiente: ${recuentos.pendiente} · en curso: ${recuentos['en-curso']} · hecha: ${recuentos.hecha}`
};
}
const estados = ['en-curso', 'pendiente', 'pendiente', 'hecha', 'en-curso', 'pendiente'];
const contador = crearContadorDeEstado(estados);
console.log(contador.resumen()); // pendiente: 3 · en curso: 2 · hecha: 1
console.log(contador.recuento('hecha'));// 1
contador.registrar('hecha');
console.log(contador.resumen()); // pendiente: 3 · en curso: 2 · hecha: 2
console.log(contador.recuentos); // undefined ← privado
contador.registrar('archivada'); // ✗ Error: Estado desconocido: archivadaComentario: recuentos y sumar son privados; la interfaz pública solo permite operaciones válidas. Nadie puede escribir contador.recuentos.hecha = 99 porque recuentos no existe fuera. Y el throw mantiene el estado siempre coherente, aplicando el principio de fallar pronto de Manejo de Errores.
Ejercicio 3
function memoizar(funcion) {
const cache = {};
return function (argumento) {
const clave = String(argumento);
if (clave in cache) return cache[clave];
const resultado = funcion(argumento);
cache[clave] = resultado;
return resultado;
};
}
// Función deliberadamente costosa
function calcularEsfuerzo(prioridad) {
let acumulado = 0;
for (let i = 0; i < 20000000; i++) {
acumulado += i % 3;
}
if (prioridad === 'alta') return 3 + (acumulado % 1);
if (prioridad === 'media') return 2 + (acumulado % 1);
return 1 + (acumulado % 1);
}
const esfuerzoRapido = memoizar(calcularEsfuerzo);
let t = Date.now();
console.log(esfuerzoRapido('alta'), `${Date.now() - t} ms`); // 3 ~120 ms
t = Date.now();
console.log(esfuerzoRapido('alta'), `${Date.now() - t} ms`); // 3 0 ms
t = Date.now();
console.log(esfuerzoRapido('media'), `${Date.now() - t} ms`); // 2 ~120 msComentario: la segunda llamada con 'alta' es prácticamente instantánea porque no ejecuta el bucle. String(argumento) normaliza la clave para que 3 y '3' compartan entrada (algo que en este caso queremos, pero que en otros podría ser un problema: es la limitación de usar un objeto como caché con claves de texto). La restricción a un parámetro no es casual: memoizar con varios exige serializar todos los argumentos en una clave, algo que resolverás con JSON.stringify en JSON y Copias de Objetos.
Conclusión
Ya sabes dónde vive cada variable. El ámbito global es para constantes de configuración como HOY y LIMITE_SEMANAL; el ámbito de función nace y muere con cada llamada; y el ámbito de bloque, que solo respetan let y const, mantiene las variables dentro de sus llaves. La cadena de ámbitos busca cada nombre de dentro hacia fuera y se detiene en la primera coincidencia, lo que explica el sombreado y el error clásico del parámetro que tapa una constante global. Y sobre todo sabes que el ámbito es léxico: depende de dónde está escrita la función, no de quién la llama.
De ahí sale el concepto central de la lección. Un closure es una función que conserva vivo el entorno donde nació, y con él has construido cuatro herramientas que Nómada Tareas usará hasta el final: crearGeneradorDeIds(), que garantiza la R1 sin dejar ningún contador suelto; crearFiltroPorResponsable(), una fábrica que produce filtros ya configurados y combinables; crearAlmacenDeTareas(), con estado verdaderamente privado y una interfaz pública controlada; y crearContadorMemoizado(), que cachea resultados de funciones puras. También entiendes por fin el bucle con var que devuelve siempre el último índice, y sabes que un closure retiene memoria, un coste que se estudia en profundidad en Gestión de Memoria.
Pero en esta lección has dado por supuesto algo que todavía no se ha explicado: que el motor "sabe" qué variables existen en cada ámbito antes de ejecutar nada. Es lo que permite llamar a una declaración de función antes de su línea, lo que provoca que var valga undefined en lugar de dar error, y lo que hace que let y const lancen ese misterioso Cannot access before initialization que ya has visto tres veces. Todo eso es el trabajo de preparación que el motor hace en cada ámbito, y es el tema de Hoisting y el Contexto de Ejecución, donde además desmontarás la pila de llamadas que en 03-01 solo viste por encima.
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
