Closures y Scope en JavaScript Explicados Simple
Estás escribiendo una función de JavaScript que devuelve otra función. La función interna usa una variable de la función externa, pero cuando la llamas más tarde, la función externa ya ha terminado. Sin embargo, la variable sigue ahí. ¿Cómo? Esa es la magia de los closures y el scope. Muchos desarrolladores luchan con este concepto, lo que lleva a errores como valores de variables inesperados o fugas de memoria. En este artículo, desglosaremos los closures y el scope de JavaScript en términos simples con ejemplos prácticos.
¿Qué es el Scope en JavaScript?
El scope determina dónde son accesibles las variables en tu código. JavaScript tiene tres tipos principales de scope:
- Scope global: Las variables declaradas fuera de cualquier función o bloque son accesibles globalmente.
- Scope de función: Las variables declaradas dentro de una función solo son accesibles dentro de esa función.
- Scope de bloque: Las variables declaradas con
letyconstdentro de un bloque{}solo son accesibles dentro de ese bloque.
Aquí hay un ejemplo rápido:
let globalVar = 'I am global';
function myFunc() {
let functionVar = 'I am in function scope';
if (true) {
let blockVar = 'I am in block scope';
console.log(globalVar); // accessible
console.log(functionVar); // accessible
console.log(blockVar); // accessible
}
console.log(blockVar); // ReferenceError: blockVar is not defined
}
myFunc();
console.log(functionVar); // ReferenceError: functionVar is not defined
Entender el scope es crucial porque previene colisiones de variables y ayuda a gestionar la memoria.
Alcance Léxico: La Base de los Closures
JavaScript utiliza alcance léxico, lo que significa que el scope de una variable está determinado por su posición en el código fuente. Las funciones internas tienen acceso a las variables en su scope externo (envolvente). Esto también se llama alcance estático.
Considera esto:
function outer() {
let outerVar = 'outer';
function inner() {
console.log(outerVar); // 'outer'
}
inner();
}
outer();
La función inner puede acceder a outerVar porque está léxicamente dentro de outer. Esto no es dinámico; no depende de cómo se llame a la función.
¿Qué es un Closure?
Un closure es una función que recuerda su scope léxico incluso cuando la función se ejecuta fuera de ese scope. En otras palabras, un closure te da acceso al scope de una función externa desde una función interna. Los closures se crean cada vez que se crea una función, en el momento de la creación de la función.
Ejemplo:
function makeCounter() {
let count = 0;
return function() {
count++;
return count;
};
}
const counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2
Aquí, makeCounter devuelve una función interna que incrementa count. Incluso después de que makeCounter haya terminado de ejecutarse, la función devuelta todavía tiene acceso a count. Eso es un closure.
Cómo Funcionan los Closures Internamente
Cuando se crea una función, almacena una referencia a su entorno léxico (la cadena de scope). Cuando se invoca la función, utiliza ese entorno para resolver variables. El entorno se mantiene vivo mientras exista el closure.
Usos Prácticos de los Closures
Los closures no son solo teóricos; se utilizan en todas partes en JavaScript.
1. Privacidad de Datos (Patrón Módulo)
Antes de los módulos ES6, los closures se usaban para crear variables privadas:
const counterModule = (function() {
let count = 0;
return {
increment: function() { count++; },
getCount: function() { return count; }
};
})();
counterModule.increment();
console.log(counterModule.getCount()); // 1
console.log(counterModule.count); // undefined
Aquí, count es privado y solo accesible a través de los métodos devueltos.
2. Manejadores de Eventos y Callbacks
Los closures se utilizan comúnmente en manejadores de eventos para acceder a variables del scope externo:
function setupButton() {
let clickCount = 0;
document.getElementById('myButton').addEventListener('click', function() {
clickCount++;
console.log(`Button clicked ${clickCount} times`);
});
}
El closure del manejador de eventos recuerda clickCount.
3. Fábricas de Funciones
Puedes crear funciones con argumentos preestablecidos:
function multiplyBy(factor) {
return function(number) {
return number * factor;
};
}
const double = multiplyBy(2);
console.log(double(5)); // 10
Errores Comunes y Cómo Evitarlos
Los closures pueden llevar a un comportamiento inesperado si no se entienden.
Captura de Variables en Bucles
Usar var en bucles crea un único enlace compartido:
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // prints 3, 3, 3
}, 100);
}
Todos los callbacks comparten el mismo i. Para solucionarlo, usa let (scope de bloque) o un IIFE:
for (let i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // prints 0, 1, 2
}, 100);
}
Fugas de Memoria
Los closures mantienen las variables vivas. Si un closure está adjunto a un objeto de larga vida (como un elemento DOM), puede evitar la recolección de basura. Siempre elimina los listeners de eventos cuando ya no sean necesarios.
Cadena de Scope y Resolución de Variables
Cuando accedes a una variable, JavaScript busca en la cadena de scope: primero el scope actual, luego los scopes externos, hasta el scope global. Si no se encuentra, se lanza un ReferenceError.
Ejemplo:
let a = 'global';
function outer() {
let b = 'outer';
function inner() {
let c = 'inner';
console.log(a, b, c); // all accessible
}
inner();
}
outer();
La cadena de scope para inner es: inner -> outer -> global.
Comparación: var, let y const
| Palabra clave | Scope | Hoisting | Reasignable |
|---|---|---|---|
| var | Función | Sí (inicializada a undefined) | Sí |
| let | Bloque | Sí (pero no inicializada, TDZ) | Sí |
| const | Bloque | Sí (pero no inicializada, TDZ) | No |
Entender estas diferencias es clave para evitar errores relacionados con el scope.
Preguntas Frecuentes
¿Qué es un closure en JavaScript?
Un closure es una función que mantiene el acceso a su scope léxico incluso cuando se ejecuta fuera de ese scope. Permite que una función "recuerde" variables de donde fue definida.
¿Por qué son importantes los closures?
Los closures permiten la privacidad de datos, fábricas de funciones y manejadores de eventos que mantienen estado. Son fundamentales para muchos patrones de JavaScript.
¿Cómo puedo evitar errores relacionados con closures?
Usa let y const para el scope de bloque, ten cuidado con los bucles y limpia los listeners de eventos para prevenir fugas de memoria.
¿Listo para probar tu comprensión? Prueba nuestro JSON Formatter para experimentar con fragmentos de código y ver cómo funcionan los closures en tiempo real.