Closures en JavaScript: cuando una función se lleva su scope

En esta página
Enterate del próximo post
Un mail cuando publico algo nuevo. Sin spam y te podés dar de baja cuando quieras.

Un mail cuando publico algo nuevo. Sin spam y te podés dar de baja cuando quieras.
El post anterior sobre scope cerró con una promesa: qué pasa cuando una función se lleva consigo el scope en el que fue definida, incluso después de que ese scope debería haber terminado. Eso es un closure, y el ejemplo con el que cerraba el post anterior (externa e interna) alcanza para mostrarlo, con un solo cambio.
El ejemplo de scope léxico mostraba interna llamada dentro de externa, mientras el scope de externa todavía estaba activo. La pregunta que queda es qué pasa si, en cambio, externa devuelve interna sin llamarla, para que se la llame recién después.
function externa() {
let nombre = "Fernando";
function interna() {
console.log(nombre);
}
return interna;
}
const saludar = externa();
saludar();
// Fernandoexterna() se ejecuta por completo: declara nombre, define interna y devuelve esa función, sin haberla llamado todavía. En ese punto externa ya terminó de correr, del mismo modo que cualquier función que devuelve algo y sale de la pila. saludar guarda la función devuelta. Cuando se la llama, tres líneas después, sigue leyendo nombre sin ningún problema, aunque externa ya no esté en ejecución. Ese es un closure: una función que conserva acceso al scope en el que fue definida, incluso cuando ese scope ya debería haber quedado atrás.

El post anterior ya lo decía sin nombrarlo todavía: cada llamada a una función crea un scope de función propio y nuevo. Eso significa que llamar dos veces a la misma función que devuelve un closure produce dos closures independientes, cada uno con su propia copia de las variables capturadas.
function crearSaludo(nombre) {
return function () {
console.log(`Hola, ${nombre}`);
};
}
const saludoFernando = crearSaludo("Fernando");
const saludoAna = crearSaludo("Ana");
saludoFernando();
// Hola, Fernando
saludoAna();
// Hola, AnacrearSaludo se llama dos veces, con un nombre distinto cada vez. Cada llamada crea su propio scope de función, con su propio nombre, y devuelve una función nueva que queda enlazada a ese scope puntual. saludoFernando y saludoAna son dos funciones distintas, cada una con su propio closure sobre un nombre distinto: llamar a una no afecta en nada a la otra.
Un closure no guarda una copia del valor que tenía la variable en el momento en que se creó: mantiene acceso a la variable misma, tal como esté en cada momento en que se lo llama.
function crearContador() {
let cuenta = 0;
function incrementar() {
cuenta = cuenta + 1;
return cuenta;
}
return incrementar;
}
const contadorA = crearContador();
const contadorB = crearContador();
console.log(contadorA());
// 1
console.log(contadorA());
// 2
console.log(contadorB());
// 1Cada llamada a crearContador crea su propio cuenta, en cero, y devuelve su propio incrementar (misma idea que la sección anterior). Lo nuevo acá es lo que pasa al llamar dos veces al mismo closure: la primera llamada a contadorA() deja cuenta en 1; la segunda no vuelve a arrancar de cero, sigue sumando sobre el mismo cuenta que ya venía de la llamada anterior, y da 2. contadorB, en cambio, tiene su propio cuenta desde otra llamada a crearContador, así que arranca de nuevo en 1. El closure no memorizó ningún valor en el momento en que se creó: cada vez que se ejecuta, va a buscar el valor actual de cuenta en su scope, lo modifica, y ese cambio queda ahí para la próxima llamada.
cuenta nunca se declaró fuera de crearContador, así que no hay ninguna forma de leerla ni de modificarla directamente desde afuera. La única forma de tocarla es a través de la función que la captura.
function crearContador() {
let cuenta = 0;
function incrementar() {
cuenta = cuenta + 1;
return cuenta;
}
return incrementar;
}
const contador = crearContador();
console.log(contador());
// 1
console.log(cuenta);
// ReferenceError: cuenta is not definedcontador() funciona porque incrementar sigue teniendo acceso a cuenta a través del closure. Pero cuenta en sí nunca estuvo disponible en el scope global: intentar leerla directamente ahí tira el mismo ReferenceError que ya apareció en el post anterior para cualquier variable fuera de su scope. La diferencia con esos ejemplos es que acá no hay ninguna forma de esquivar el límite: no existe otra línea de código, en ningún otro lugar, que pueda leer cuenta sin pasar por incrementar. Ese aislamiento (una variable a la que solo se puede llegar a través de una función puntual) es lo que un closure permite construir a propósito, no solo un efecto secundario de cómo funciona el scope.
Un closure es una función que conserva acceso al scope en el que fue definida, incluso después de que ese scope debería haber terminado. Cada llamada a la función que lo crea arma un closure independiente, con su propia copia de las variables capturadas. El closure no memoriza un valor: sigue leyendo y modificando la variable real, así que el estado que guarda persiste entre llamadas y, si nada más puede acceder a esa variable, queda efectivamente privado.
El próximo post trata las funciones de orden superior: funciones que reciben otras funciones como argumento o que devuelven una función, como ya hicieron crearSaludo y crearContador acá mismo, sin que todavía se lo haya nombrado así.