Módulo 00 // Unidad 04

Ámbito y closures

Dónde es visible cada variable y cómo una función recuerda el lugar en el que se creó

Unidad para alumnos del curso

Comprobando tu sesión…

Inicia sesión con la cuenta de Google con la que estás matriculado para ver esta unidad.

Esa cuenta no tiene acceso a este curso. Prueba con otra.

No se ha podido cargar el inicio de sesión de Google. Comprueba la conexión o si algún bloqueador lo impide, y recarga la página.

En la unidad anterior viste una función que devolvía otra función y que, de alguna forma, seguía recordando un valor de fuera. No es un truco: es la consecuencia directa de cómo busca JavaScript las variables. Entender el ámbito y los closures te explicará comportamientos que, de otro modo, parecen magia o errores del lenguaje.

1 // Qué es el ámbito

El ámbito (scope) de una variable es la zona del código desde la que se puede usar. En JavaScript moderno hay tres niveles:

const app = 'Tienda';           // ámbito global: visible en todo el script

function calcularTotal(precios) {
  let total = 0;                // ámbito de función: sólo dentro de calcularTotal

  for (const precio of precios) {
    const conIva = precio * 1.21;  // ámbito de bloque: sólo dentro de este for
    total += conIva;
  }

  // console.log(conIva);       // ReferenceError: aquí ya no existe
  return total;
}

Con let y const, cualquier par de llaves crea un ámbito nuevo: el cuerpo de un if, de un for o un bloque suelto. Esto es igual que en Java, así que te resultará natural.

Hay un detalle adicional: aunque la variable pertenece a todo su bloque, no se puede usar antes de su declaración. Si lo intentas, obtienes un ReferenceError. A esa franja entre el inicio del bloque y la declaración se la llama zona muerta temporal.

Todo esto vale para let y const. La forma antigua de declarar, var, sigue otras reglas, y conviene verlas porque la encontrarás en código antiguo y en muchos ejemplos de internet:

function mostrarNotas(notas) {
  for (var i = 0; i < notas.length; i++) {
    var nota = notas[i];
    if (nota >= 5) {
      var mensaje = 'Hay algún aprobado';
    }
  }

  console.log(i);        // 3: i sigue existiendo fuera del for
  console.log(nota);     // 8: nota también
  console.log(mensaje);  // 'Hay algún aprobado': y mensaje, fuera del if y del for
}

mostrarNotas([4, 7, 8]);

Con let, cada una de esas tres líneas finales lanzaría ReferenceError, porque las variables sólo existían dentro de sus llaves. Con var, el único límite es la función: da igual en qué bloque se declare.

La segunda rareza es que una variable var se puede usar antes de la línea que la declara. El motor registra la declaración al entrar en la función (el mismo hoisting que viste con las funciones), pero el valor no se asigna hasta llegar a su línea:

console.log(total);    // undefined: no hay error
var total = 10;
console.log(total);    // 10

console.log(precio);   // ReferenceError: Cannot access 'precio' before initialization
let precio = 10;
Aspecto let y const var
Ámbito El bloque: las llaves más cercanas La función entera
Uso antes de declarar ReferenceError Vale undefined, sin error
Declarar dos veces el mismo nombre Error de sintaxis Se permite en silencio
En un bucle for Una variable nueva por vuelta Una sola variable para todo el bucle

var no respeta los bloques: una variable declarada con var dentro de un if o un for es visible en toda la función. Además, se puede usar antes de declararla y vale undefined sin dar error. Es la razón principal por la que en código moderno no se usa var.

2 // La cadena de ámbitos

Los ámbitos se anidan como cajas unas dentro de otras. Cuando el código usa una variable, el motor la busca siguiendo siempre el mismo recorrido:

  1. En el ámbito actual.
  2. Si no está, en el ámbito que lo contiene.
  3. Y así hacia fuera, hasta el ámbito global.
  4. Si no aparece en ninguno, lanza ReferenceError.

La búsqueda va siempre de dentro hacia fuera, nunca al revés: una función interior ve las variables de la exterior, pero la exterior no ve las de la interior.

Lo decisivo es que esta cadena se decide por dónde está escrita la función en el código, no por desde dónde se llama. Por eso se habla de ámbito léxico: basta con leer el código y ver qué llaves rodean a cada función para saber qué variables puede usar.

ÁMBITOS ANIDADOS Y BÚSQUEDA DE VARIABLES ámbito global const contador = crearContador(); function crearContador() let cuenta = 0; return () => { ... } cuenta++; return cuenta; no está aquí: se busca hacia fuera ORDEN DE BÚSQUEDA 1. ámbito propio 2. ámbito que lo contiene 3. ... hasta el global 4. ReferenceError CLOSURE la función devuelta conserva el acceso a cuenta aunque crearContador ya acabó
La flecha verde es la búsqueda: cuenta no existe dentro de la función devuelta, así que se encuentra un nivel más fuera. La columna de la derecha explica por qué ese acceso sobrevive.

3 // Closures: funciones con memoria

Junta ahora dos ideas: una función puede devolver otra función, y cada función puede usar las variables de los ámbitos que la rodean. ¿Qué pasa cuando la función exterior termina?

function crearContador() {
  let cuenta = 0;
  return () => {
    cuenta++;
    return cuenta;
  };
}

const contadorA = crearContador();
const contadorB = crearContador();

contadorA(); // 1
contadorA(); // 2
contadorB(); // 1  (otra cuenta distinta)
contadorA(); // 3

En Java, las variables locales de un método desaparecen al terminar. Aquí, crearContador ya ha terminado, pero la función devuelta todavía necesita cuenta, así que el motor la conserva.

Esa es la definición: un closure (en español, clausura) es una función junto con las variables del ámbito en el que se creó. Toda función de JavaScript queda enganchada al sitio donde nació, y mantiene vivas las variables de ese sitio que usa mientras la propia función exista, aunque el código que las declaró haya terminado hace rato.

Una imagen que ayuda: cada función lleva una mochila con las variables de fuera que usa. Cuando crearContador devuelve la flecha, la flecha sale con la mochila puesta, y dentro va cuenta.

UN CLOSURE: LA FUNCIÓN Y SU MOCHILA función + variables de donde se creó = closure contadorA ()=>++cuenta mochila: { cuenta } 2 llamadas cuenta = 2 contadorB ()=>++cuenta mochila: { cuenta } 1 llamada cuenta = 1 crearContador() ya terminó: cada mochila vive mientras exista su función
Cada llamada a crearContador fabrica una función con su propia mochila. Por eso contadorA y contadorB llevan cada uno una cuenta distinta, aunque el código que las creó ya no se esté ejecutando.

Tres detalles completan la idea:

  • Cada llamada crea una mochila nueva. Llamar a crearContador() crea un ámbito nuevo, con su propia cuenta. Por eso contadorA y contadorB no se interfieren.
  • La mochila guarda la variable, no una copia de su valor. Si la función cambia cuenta, la siguiente llamada ve el cambio; por eso el contador avanza.
  • Desde fuera no se puede abrir. No existe contadorA.cuenta: la única forma de llegar a esa variable es llamar a la función.

Un closure permite tener estado privado sin clases: cuenta sólo se puede modificar a través de la función devuelta. Nadie más en el programa puede ponerla a cero o cambiarla por error.

4 // Dónde vas a encontrar closures

No vas a escribir crearContador todos los días, pero sí vas a usar closures continuamente sin darte cuenta. Cada vez que un callback usa una variable de fuera, hay un closure:

function configurarBoton(boton, mensaje) {
  let pulsaciones = 0;
  boton.addEventListener('click', () => {
    pulsaciones++;
    console.log(`${mensaje} (${pulsaciones})`);
  });
}

La función del click se ejecutará mucho después de que configurarBoton termine, y aun así tendrá acceso a mensaje y pulsaciones. Lo mismo ocurre con los callbacks de setTimeout, con las promesas o con los componentes de Vue.

Un bucle programa tres mensajes con setTimeout para que aparezcan uno por segundo:

for (var i = 1; i <= 3; i++) {
  setTimeout(() => console.log(`Mensaje ${i}`), i * 1000);
}

El resultado es Mensaje 4 tres veces.

  1. Cuándo se ejecutan los callbacks: setTimeout no espera; programa la función y el bucle sigue. Los tres callbacks se ejecutan cuando el bucle ya terminó.
  2. Cuántas i hay: con var, la variable pertenece a la función entera, así que existe una sola i compartida por los tres closures.
  3. Cuánto vale al final: el bucle sale cuando i llega a 4. Esa es la i que ven los tres callbacks.
  4. Qué cambia con let: let crea una variable nueva en cada vuelta del bucle. Cada callback cierra sobre la i de su vuelta.
Cambiando var por let se obtiene Mensaje 1, Mensaje 2 y Mensaje 3. El código no cambia de lógica: cambia cuántas variables hay para que los closures recuerden.

5 // Para practicar

Predice qué valores imprime un script que mezcla var y let, y compruébalo después. Cada ejercicio trae el enunciado en los comentarios del código.

Sombreado de variables y var en buclestraining-daw-sv/modulo-00/04-ambito-y-closures/01_scopes

Autoevaluación

Una función devuelta por crearContador() sigue usando la variable cuenta aunque crearContador ya terminó. ¿Por qué no se ha perdido esa variable?
Porque la función devuelta mantiene una referencia a su ámbito de origen. Mientras exista algo que pueda usar cuenta, el motor no la elimina. Eso es un closure: una función junto con las variables del lugar donde se definió.
Dos contadores creados con dos llamadas a crearContador() no comparten la cuenta. ¿Qué lo explica?
Cada llamada crea un ámbito nuevo con su propia variable cuenta. La primera función devuelta se queda con la cuenta de la primera llamada y la segunda, con la de la segunda. Son dos variables distintas que se llaman igual.
Un bucle for (var i = 0; i < 3; i++) programa tres setTimeout que muestran i, y los tres muestran 3. Con let muestran 0, 1 y 2. Explica la diferencia.
var crea una sola variable para todo el bucle; let crea una por vuelta. Los callbacks se ejecutan cuando el bucle ya terminó. Con var los tres ven la misma i, que ya vale 3; con let cada callback se quedó con la i de su vuelta.