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:

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

  1. La función crearSaludo devuelve una función interna que utiliza el argumento saludo.
  2. Aunque crearSaludo ya terminó su ejecución, la función interna aún tiene acceso a la variable saludo 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.

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

    javascript
    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 variable cuenta y retorna una función que incrementa esa variable.
    • La variable cuenta no se pierde cuando contador 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:

    1. Encapsulamiento: Permiten ocultar detalles de implementación. Por ejemplo, puedes crear funciones privadas que no estén disponibles fuera de un determinado entorno.
    2. Funciones de Retorno: Permiten crear funciones personalizadas, puedes crear funciones dinámicas que conservan el estado del entorno donde fueron definidas.
    3. 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.

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

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

    javascript
    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

    1. Gestión de memoria:
      Como los closures mantienen referencias a su entorno, pueden causar problemas de memoria si no se manejan adecuadamente.
    2. Evitar el abuso:
      Usar demasiados closures en un proyecto grande puede dificultar la legibilidad del código.
    3. 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.

    +1
    0
    +1
    0