Cómo Funcionan los Closures en JavaScript
Has escrito una función dentro de otra función y, de alguna manera, la función interna sigue accediendo a variables de la externa, incluso después de que la función externa haya terminado de ejecutarse. Este comportamiento confunde a muchos desarrolladores, pero sigue una regla simple: los closures capturan bindings, no valores.
Este artículo explica cómo funcionan los closures en JavaScript, qué significa realmente el ámbito léxico y cómo evitar errores comunes que surgen de malinterpretar estos mecanismos.
Puntos Clave
- Un closure es una función combinada con su entorno léxico: los bindings de variables que existían cuando se creó la función.
- Los closures capturan bindings (referencias), no valores, por lo que las mutaciones a las variables capturadas permanecen visibles.
- Usa
letoconsten bucles para crear bindings nuevos por iteración y evitar el problema clásico de los bucles. - Los problemas de memoria provienen de retener referencias innecesarias, no de los closures en sí mismos.
¿Qué es un closure?
Un closure es una función combinada con su entorno léxico: el conjunto de bindings de variables que existían cuando se creó la función. Cada función en JavaScript forma un closure en el momento de su creación.
function createGreeter(greeting) {
return function(name) {
return `${greeting}, ${name}`
}
}
const sayHello = createGreeter('Hello')
sayHello('Alice') // "Hello, Alice"
Cuando createGreeter retorna, su contexto de ejecución termina. Sin embargo, la función retornada todavía accede a greeting. La función interna no copió la cadena “Hello”, sino que retuvo una referencia al binding mismo.
El ámbito léxico en JavaScript explicado
El ámbito léxico significa que el acceso a variables se determina por dónde se definen las funciones en el código fuente, no por dónde se ejecutan. El motor de JavaScript resuelve los nombres de variables recorriendo la cadena de ámbitos desde el ámbito más interno hacia afuera.
const multiplier = 2
function outer() {
const multiplier = 10
function inner(value) {
return value * multiplier
}
return inner
}
const calculate = outer()
calculate(5) // 50, no 10
La función inner usa multiplier de su entorno léxico, el ámbito donde fue definida, independientemente de cualquier variable global con el mismo nombre.
Los closures capturan bindings, no instantáneas
Un error común es pensar que los closures “congelan” los valores de las variables. No lo hacen. Los closures mantienen referencias a bindings, por lo que las mutaciones permanecen visibles:
function createCounter() {
let count = 0
return {
increment() { count++ },
getValue() { return count }
}
}
const counter = createCounter()
counter.increment()
counter.increment()
counter.getValue() // 2
Ambos métodos comparten el mismo binding de count. Los cambios realizados por increment son visibles para getValue porque referencian la misma variable.
El problema clásico de los bucles: var versus let
Esta distinción es más importante en los bucles. Con var, todas las iteraciones comparten un binding:
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100)
}
// Imprime: 3, 3, 3
Cada callback cierra sobre la misma i, que es igual a 3 cuando se ejecutan los callbacks.
Con let, cada iteración crea un binding nuevo:
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100)
}
// Imprime: 0, 1, 2
El ámbito de bloque le da a cada closure su propia i.
Discover how at OpenReplay.com.
Patrones prácticos: funciones factory y manejadores de eventos
Los closures permiten funciones factory que producen comportamiento especializado:
function createValidator(minLength) {
return function(input) {
return input.length >= minLength
}
}
const validatePassword = createValidator(8)
validatePassword('secret') // false
validatePassword('longenough') // true
Los manejadores de eventos usan closures naturalmente para retener contexto:
function setupButton(buttonId, message) {
document.getElementById(buttonId).addEventListener('click', () => {
console.log(message)
})
}
El callback retiene acceso a message mucho después de que setupButton retorne.
Consideraciones de memoria: qué retienen realmente los closures
Los closures no causan fugas de memoria inherentemente. Los problemas surgen cuando las funciones retienen inadvertidamente referencias a objetos grandes o estructuras de datos de larga duración.
function processData(largeDataset) {
const summary = computeSummary(largeDataset)
return function() {
return summary // Solo retiene summary, no largeDataset
}
}
Si tu closure solo necesita una pequeña porción de datos, extráela antes de crear la función interna. El objeto grande original se vuelve elegible para la recolección de basura.
Los motores modernos de JavaScript optimizan los closures agresivamente. Son una característica normal del lenguaje, no una preocupación de rendimiento en el uso típico. El problema no son los closures en sí mismos, sino retener referencias que no necesitas.
Construyendo un modelo mental confiable
Piensa en los closures de esta manera: cuando se crea una función, captura una referencia a su ámbito circundante. Ese ámbito contiene bindings (conexiones entre nombres y valores), no los valores en sí mismos. La función puede leer y modificar esos bindings durante toda su vida útil.
Este modelo explica por qué las mutaciones son visibles, por qué let soluciona los problemas de bucles y por qué los closures funcionan a través de límites asíncronos. La función y su entorno léxico viajan juntos.
Conclusión
Los closures son funciones empaquetadas con su entorno léxico. Capturan bindings, no valores, por lo que los cambios a las variables capturadas permanecen visibles. Usa let o const en bucles para crear bindings nuevos por iteración. Los problemas de memoria provienen de retener referencias innecesarias, no de los closures en sí mismos.
Comprender el ámbito y los closures de JavaScript te da una base para razonar sobre el tiempo de vida de las variables, la encapsulación de datos y el comportamiento de los callbacks, patrones que encontrarás diariamente en el desarrollo frontend.
Preguntas Frecuentes
Cada función en JavaScript es técnicamente un closure porque captura su entorno léxico en el momento de la creación. El término closure típicamente se refiere a funciones que acceden a variables de un ámbito externo después de que ese ámbito haya terminado de ejecutarse. Una función que solo usa sus propias variables locales o variables globales no demuestra el comportamiento de closure de manera significativa.
Los closures no causan fugas de memoria inherentemente. Los problemas ocurren cuando los closures retienen inadvertidamente referencias a objetos grandes o estructuras de datos que deberían ser recolectadas por el recolector de basura. Para evitar esto, extrae solo los datos que tu closure necesita antes de crear la función interna. Los motores modernos de JavaScript optimizan los closures agresivamente, por lo que no son una preocupación de rendimiento en el uso típico.
Esto sucede cuando usas var en bucles porque var tiene ámbito de función, no ámbito de bloque. Todas las iteraciones comparten el mismo binding, por lo que los callbacks ven el valor final cuando se ejecutan. Soluciona esto usando let en su lugar, que crea un binding nuevo para cada iteración. Cada closure entonces captura su propia copia de la variable del bucle.
Sí. Los closures capturan bindings (referencias a variables), no instantáneas de valores. Si la variable capturada cambia, el closure ve el valor actualizado. Por esto es que múltiples funciones que comparten el mismo closure pueden comunicarse a través de variables compartidas, como se ve en el patrón de contador donde los métodos increment y getValue comparten el mismo binding de count.
Complete picture for complete understanding
Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.
Check our GitHub repo and join the thousands of developers in our community.