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

  1. Qué es el ámbito
  2. Ámbito global
  3. Ámbito de función
  4. Ámbito de bloque: let y const frente a var
  5. La cadena de ámbitos, paso a paso
  6. Sombreado de variables (shadowing)
  7. Qué es un closure
  8. El closure explicado con un ejemplo mínimo
  9. El clásico bucle con var frente a let
  10. Patrón 1: generador de identificadores
  11. Patrón 2: fábrica de funciones configuradas
  12. Patrón 3: estado privado con el patrón módulo
  13. Patrón 4: memoización sencilla
  14. El coste en memoria
  15. Errores Comunes y Consejos
  16. Ejercicios
  17. Conclusión

  1. 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.

  1. Á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));   // 15

Las 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.

  1. Á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 defined

Y 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 nada

Esa amnesia es lo que los closures del apartado 7 permiten romper cuando conviene.

  1. Ámbito de bloque: let y const frente a var

Un 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 No
Se puede redeclarar en el mismo ámbito (peligroso) No No
Antes de la línea de declaración undefined Error (TDZ) Error (TDZ)
Se añade a globalThis si es global 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 declared

En 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.

  1. 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-20

Así 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:

  1. La búsqueda es unidireccional: hacia arriba. Desde generarInforme no se puede ver linea.
  2. Para en la primera coincidencia. Si detallar declarara su propio cabecera, la búsqueda se detendría ahí (apartado 6).
  3. 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.

  1. 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);          // global

El 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 dentro

Quien 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: tarea fuera y tareaOriginal dentro, por ejemplo.

  1. 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

  1. 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, Marta

Lee bien la secuencia:

  1. Se llama a crearSaludo('Marta') y se crea un ámbito con nombre y saludo.
  2. Se crea la función interna, que referencia a saludo.
  3. crearSaludo termina y devuelve esa función.
  4. Normalmente saludo desaparecería. Pero como la función devuelta lo necesita, el motor conserva el entorno.
  5. Al llamar a saludarAMarta(), la función encuentra su saludo intacto.

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ía

Dos 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.

  1. El clásico bucle con var frente a let

Este 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 reservas
flowchart 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é.

  1. 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'  ← protegida

Compara 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

  1. 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));   // true

Y 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.

  1. 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' ← privada

Las garantías que ofrece esta estructura son reales, no de estilo:

  • Nadie puede hacer almacen.tareas.push(basura), porque tareas no está expuesto.
  • ultimoId no se puede alterar, así que la R1 (identificadores únicos y correlativos) está garantizada por construcción.
  • buscarIndice es 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.

  1. 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:

  1. 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.
  2. 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.

  1. 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` exista

Los 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 entonces

2. 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();   // global

7. 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

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: archivada

Comentario: 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 ms

Comentario: 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

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