Closures y Ámbito Léxico en JavaScript
En JavaScript, uno de los conceptos más potentes y utilizado es el de los closure. Estos permiten que las funciones “recuerden” el entorno en el que fueron creadas, incluso después de que ese entorno haya dejado de existir. Aunque pueda parecer un detalle técnico, los closures tienen un impacto significativo en cómo se estructura, organiza y ejecuta el código.
Además, el concepto de ámbito léxico juega un papel clave en el funcionamiento de los closures. Este artículo explica en qué son los closures, cómo funcionan y por qué son esenciales para escribir código eficiente y modular en JavaScript.
¿Qué es un Closure?
Un closure es la combinación de una función y el entorno léxico dentro del cual fue declarada. Dicho de manera más simple: Un closure permite que una función acceda a las variables de su entorno exterior, incluso después de que la función externa haya terminado su ejecución.
Ejemplo básico:
function crearSaludo(saludo) {
return function(nombre) {
return `${saludo}, ${nombre}!`;
};
}
const saludoEnEspanol = crearSaludo("Hola");
console.log(saludoEnEspanol("Carlos"));
const saludoEnIngles = crearSaludo("Hello");
console.log(saludoEnIngles("John"));
En el anterior ejemplo:
- La función
crearSaludo
devuelve una función interna que utiliza el argumentosaludo
. - Aunque
crearSaludo
ya terminó su ejecución, la función interna aún tiene acceso a la variablesaludo
gracias al closure.
Ámbito Léxico en JavaScript
El ámbito léxico determina qué variables son accesibles dentro de una función según el lugar donde fue declarada en el código. En JavaScript, este ámbito es estático, lo que significa que se establece durante la definición de la función y no cambia cuando se ejecuta en otro contexto.
let mensaje = "Hola, mundo!";
function mostrarMensaje() {
console.log(mensaje);
}
mostrarMensaje();
En el anterior snippet la función mostrarMensaje
puede acceder a la variable mensaje
porque ambas fueron definidas en el mismo ámbito léxico (el global). Incluso si invocamos mostrarMensaje
en otro contexto, siempre tendrá acceso a mensaje
.
Closures en Funciones Internas
Cuando una función interna (o anidada) tiene acceso a las variables de su función externa, estamos viendo un ejemplo claro de un closure.
function contador() {
let cuenta = 0;
return function() {
cuenta++;
return cuenta;
};
}
const incrementar = contador();
console.log(incrementar());
console.log(incrementar());
console.log(incrementar());
contador
define la variablecuenta
y retorna una función que incrementa esa variable.- La variable
cuenta
no se pierde cuandocontador
termina su ejecución, porque la función retornada sigue “recordando” el entorno léxico en el que fue creada. Este es el closure en acción.
Beneficios de los Closures
Los closures no solo son interesantes desde el punto de vista técnico, también ofrecen múltiples beneficios prácticos:
- Encapsulamiento: Permiten ocultar detalles de implementación. Por ejemplo, puedes crear funciones privadas que no estén disponibles fuera de un determinado entorno.
- Funciones de Retorno: Permiten crear funciones personalizadas, puedes crear funciones dinámicas que conservan el estado del entorno donde fueron definidas.
- Manejo de Estado: Los closures permiten que las funciones mantengan un estado sin tener que recurrir a variables globales o usar objetos.
Closures y Bucles
Uno de los casos donde los closures pueden ser muy útiles es cuando trabajamos con bucles. Sin closures, los bucles pueden generar resultados inesperados al trabajar con funciones asincrónicas como setTimeout
.
for (var i = 1; i <= 3; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
¿Qué crees que sucede? La salida del ejemplo anterior es 4
, 4
, 4
. Esto ocurre porque la variable i
es compartida por todos los callbacks de setTimeout
y cuando estos se ejecutan, el bucle ya ha terminado, por lo que el valor de i
es 4.
for (var i = 1; i <= 3; i++) {
(function(x) {
setTimeout(function() {
console.log(x);
}, 1000);
})(i);
}
Con este ajuste, la salida será 1
, 2
, 3
, como se esperaría. En este caso cada iteración crea un nuevo ámbito léxico que captura el valor actual de i
.
Encapsulación con closures
Los closures permiten crear funciones privadas que limitan el acceso a ciertos datos.
function banco() {
let saldo = 1000;
return {
consultar: function() {
return `Saldo actual: $${saldo}`;
},
depositar: function(cantidad) {
saldo += cantidad;
return `Nuevo saldo: $${saldo}`;
},
retirar: function(cantidad) {
if (cantidad > saldo) return "Fondos insuficientes.";
saldo -= cantidad;
return `Nuevo saldo: $${saldo}`;
}
};
}
const miBanco = banco();
console.log(miBanco.consultar());
console.log(miBanco.depositar(200));
console.log(miBanco.retirar(1500));
En este ejemplo, saldo
está encapsulado y no es accesible directamente desde fuera de la función banco
.
Consideraciones al usar closures
- Gestión de memoria:
Como los closures mantienen referencias a su entorno, pueden causar problemas de memoria si no se manejan adecuadamente. - Evitar el abuso:
Usar demasiados closures en un proyecto grande puede dificultar la legibilidad del código. - Cuidado con variables compartidas:
Cuando múltiples closures acceden a la misma variable, pueden surgir resultados inesperados si no se controlan adecuadamente.
Conclusión
Los closures son una herramienta esencial en JavaScript. Permiten que las funciones accedan a variables en su entorno léxico y son fundamentales para crear código más modular y eficiente. Desde mantener estados hasta crear funciones personalizadas y seguras.
En el siguiente vemos los parámetros por defecto en JavaScript, que nos permiten inicializar parámetros con valores predeterminados en las funciones.