Entendiendo los Closures en JavaScript

Un closure en JavaScript es una combinación de una función y el entorno léxico en el que se declaró dicha función. Este entorno incluye cualquier varible local que estuviera en el ámbito cuando el closure fue creado.

function configurarSaludo() {
    let mensaje = "Hola"; // Variable local de configurarSaludo

    function mostrarSaludo() { // mostrarSaludo es una función interna, un closure
        console.log(mensaje); // Accede a la variable declarada en la función externa
    }

    mostrarSaludo();
}

configurarSaludo(); // Ejecutar la función externa

Cuando se ejecuta mostrarSaludo(), esta función puede acceder al valor de mensaje gracias a la cadena de ámbitos (scope chain). Los closures nos permiten preservar el estado de variables como mensaje, lo cual se basa en la característica de alcance léxico de JavaScript. El alcance léxico se determina en el momento en que la función es definida. Cuando una función interna se convierte en un closure, mantiene su propia cadena de ámbitos.

Desventajas de los Closures

Normalmente, el ámbito de una función y sus variables se liberan de la memoria después de que la función termina su ejecución. Sin embargo, si se crea un closure, el ámbito de la función externa que contiene al closure se mantendrá activo mientras el closure exista, lo que puede generar un consumo de memoria adicional.

function crearContador() {
    let contador = 0; // Variable que se mantendrá en memoria

    function incrementar() { // Este es el closure
        contador++;
        console.log(contador);
    }

    return incrementar; // Devolvemos la función interna
}

const miContador = crearContador(); // miContador ahora referencia a la función incrementar
miContador(); // Muestra 1
miContador(); // Muestra 2

// Para liberar la memoria, podemos asignar null a la variable que referencia al closure:
// miContador = null;

Casos de Uso Comunes para Closures

1. Emulación de Métodos Privados y Públicos

Los closures son útiles para crear funciones públicas que pueden acceder y manipular variables "privadas" dentro de un ámbito encapsulado. Aunque ES6 ofrece clases y ES5 tiene constructores para esto, los closures proporcionan una alternativa.

const GestorUsuario = function() {
    // Variables "privadas"
    let nombreUsuario = "";
    let emailUsuario = "";

    // Devolvemos un objeto con métodos "públicos"
    return {
        obtenerNombre: function() {
            return nombreUsuario;
        },
        establecerNombre: function(nuevoNombre) {
            nombreUsuario = nuevoNombre;
        },
        obtenerEmail: function() {
            return emailUsuario;
        },
        establecerEmail: function(nuevoEmail) {
            emailUsuario = nuevoEmail;
        }
    };
};

const usuario = GestorUsuario(); // Se crea un entorno léxico compartido por los métodos públicos
usuario.establecerNombre("Alice");
usuario.establecerEmail("alice@example.com");
console.log(usuario.obtenerNombre()); // Muestra "Alice"
console.log(usuario.obtenerEmail());  // Muestra "alice@example.com"

En este ejemplo, usuario.obtenerNombre y usuario.establecerNombre son closures que comparten el mismo entorno léxico creado por GestorUsuario.

2. Solución al Problema del Índice en Bucles

Un problema común surge al intentar acceder al valor de un índice de bucle dentro de un manejador de eventos, especialmente con var.

// Ejemplo problemático
// Supongamos que tenemos 3 botones
const botones = document.querySelectorAll("button");
for (var j = 0; j < botones.length; j++) {
    botones[j].addEventListener('click', function() {
        console.log(j); // Esto mostrará el valor final de j (ej: 3) para todos los botones
    });
}

// Solución usando closures con una IIFE (Immediately Invoked Function Expression)
for (var j = 0; j < botones.length; j++) {
    (function(indiceActual) {
        botones[j].addEventListener('click', function() {
            console.log(indiceActual); // Muestra el índice correcto (0, 1, 2)
        });
    })(j); // Pasamos el valor actual de j a la IIFE
}

// Solución usando una función fábrica (Function Factory)
function crearManejadorClick(indice) {
    return function() {
        console.log(indice); // Accede al 'indice' del ámbito de crearManejadorClick
    };
}

for (var j = 0; j < botones.length; j++) {
    botones[j].addEventListener('click', crearManejadorClick(j));
}

// Otro ejemplo común con setTimeout
// Ejemplo problemático
for (var k = 0; k < 3; k++) {
    setTimeout(function() {
        console.log("Problemático:", k); // Muestra 3, 3, 3
    }, 100);
}

// Solución con closure (IIFE)
for (var k = 0; k < 3; k++) {
    (function(indiceActual) {
        setTimeout(function() {
            console.log("Con IIFE:", indiceActual); // Muestra 0, 1, 2
        }, 100);
    })(k);
}

Etiquetas: JavaScript closures Scope lexical scope IIFE

Publicado el 7-9 21:22