Menu

Closures en JavaScript: qué son y cómo funcionan

Un closure es una función que recuerda las variables de su entorno. Te explico cómo funcionan los closures en JavaScript con ejemplos y casos reales.

Un closure es una función que recuerda

Cada vez que defines una función en JavaScript, esta guarda en silencio una conexión con las variables que la rodean. Cuando la función se ejecuta más tarde —aunque sea en un lugar totalmente distinto—, sigue pudiendo ver esas variables. Eso es un closure en JavaScript.

La demo más corta posible:

index.js
Output
Click Run to see the output here.

makeGreeter se ejecuta, devuelve una función interna y termina. Uno esperaría que su variable local name desapareciera, porque la función ya acabó. Pero como la función interna sigue usando name, JavaScript la mantiene viva. greetAda recuerda "Ada" y greetBoris recuerda "Boris". Dos closures, dos valores recordados distintos.

El scope léxico: el origen de los closures en JavaScript

La regla que hay detrás de los closures se llama scope léxico (o ámbito léxico): una función ve las variables del lugar donde fue escrita, no del lugar desde donde se la llama. "Léxico" simplemente quiere decir "según la posición en el código fuente".

index.js
Output
Click Run to see the output here.

show imprime "estoy fuera", no "estoy dentro de caller". Se escribió junto al outer de nivel superior, así que es ese el que ve. Da igual que la llamada venga desde un sitio que tenga su propio outer.

Los closures no son más que ámbito léxico que sobrevive a la función externa. La variable no desaparece porque alguien todavía conserva una referencia a ella.

Cada llamada crea su propio closure

Cada vez que llamas a la función externa se crean variables nuevas, y cualquier función interna devuelta desde esa llamada recuerda esas variables en concreto. Por eso greetAda y greetBoris del ejemplo anterior no se pisaban entre sí.

El ejemplo clásico es un contador con closure:

index.js
Output
Click Run to see the output here.

a y b tienen cada uno su propio count. Nada fuera de la función devuelta puede tocar esas variables: count es completamente privado. Y ojo, esto no es una característica del lenguaje que hayamos activado; surge de forma natural por cómo funcionan los closures.

Variables privadas con closures (sin usar clases)

Como a las variables encerradas solo se llega a través de la función devuelta, puedes aprovechar los closures para construir pequeños objetos con estado realmente privado:

index.js
Output
Click Run to see the output here.

balance no es una propiedad del objeto que devolvemos: vive dentro del closure. La única forma de leerlo o modificarlo es a través de los métodos que expusiste. Las clases con campos #private también permiten hacer esto, pero la versión con closures las precede por décadas y sigue apareciendo por todo el ecosistema.

El clásico problema del closure dentro de un bucle

Donde más suelen tropezar los desarrolladores con los closures es dentro de los bucles. Mira lo que pasa al usar var:

index.js
Output
Click Run to see the output here.

Lo lógico sería esperar 0, 1, 2, pero el resultado es 3, 3, 3. La razón es sencilla: var tiene ámbito de función, así que existe una única i compartida en todo el bucle. Los tres closures capturaron esa misma variable y, cuando por fin se ejecutan, el bucle ya terminó y i vale 3.

La solución es usar let:

index.js
Output
Click Run to see the output here.

Ahora imprime 0, 1 y 2. La clave está en que let tiene ámbito de bloque: cada iteración del bucle crea un nuevo binding para i, así que cada closure captura su propio valor. Esta es, sin duda, la mayor razón para preferir let frente a var.

Los closures capturan variables, no valores

Un detalle sutil pero fundamental: un closure se queda con la variable en sí, no con una foto de su valor en el momento en que se definió la función.

index.js
Output
Click Run to see the output here.

printMessage lee message en el momento de ejecutarse, no cuando se creó. Si quieres una instantánea del valor, primero cópialo en una variable local — que es básicamente lo que hace let dentro de un bucle for.

Un patrón habitual en la práctica: Once

Aquí tienes una pequeña utilidad que aprovecha un closure para garantizar que una función se ejecute una única vez:

index.js
Output
Click Run to see the output here.

called y result son estado privado que vive mientras viva la función retornada. Sin banderas globales, sin objetos extra. Este patrón —una pequeña función auxiliar, estado privado y un closure— es una de las cosas más útiles que puedes hacer en JavaScript.

Una nota sobre la memoria

Un closure mantiene vivas sus variables capturadas mientras algo siga referenciando al closure. Normalmente eso es justo lo que quieres, pero ojo: si asocias un closure a algo de larga vida (como un event listener del DOM o una caché global) y ese closure captura algo pesado, ese "algo" no podrá ser recolectado por el garbage collector hasta que el closure desaparezca.

function attach() {
    const hugeData = new Array(1_000_000).fill("...");
    document.addEventListener("click", () => {
        console.log(hugeData.length);
    });
}

Mientras el listener siga conectado, hugeData permanece en memoria. Si quitas el listener (o evitas capturar lo que no necesitas), esa referencia se libera. No hace falta que lo controles al milímetro, pero sí conviene tener claro que los closures y la memoria van de la mano.

Lo que te llevas

  • Un closure es una función más las variables que veía cuando se definió.
  • Cada llamada a la función externa crea un juego nuevo de variables para sus closures internos.
  • Los closures te permiten tener estado privado sin recurrir a clases.
  • Dentro de un bucle, usa let para que cada iteración tenga su propio binding.
  • Los closures capturan la variable en sí, no el valor que tenía al momento de crearse.

Siguiente tema: la palabra clave this

Los closures se encargan de las variables que rodean a una función. La siguiente pieza es sobre qué se invoca una función, algo que en JavaScript se controla con this y que se comporta de forma muy distinta a las variables capturadas que acabamos de ver.

Preguntas frecuentes

¿Qué es un closure en JavaScript?

Un closure es una función que recuerda las variables del ámbito en el que fue definida, incluso cuando ese ámbito externo ya terminó de ejecutarse. Técnicamente, en JavaScript toda función es un closure, pero el término suele usarse cuando una función se devuelve o se pasa a otro sitio y sigue accediendo a las variables de su scope original.

¿Para qué sirven los closures?

Permiten que una función lleve consigo su propio estado privado. Tienes datos ligados a esa función sin necesidad de crear una clase ni una variable global. Son habituales en contadores, callbacks que se ejecutan una sola vez, funciones memoizadas y para ocultar detalles de implementación detrás de una API pequeña.

¿Por qué los closures se comportan raro dentro de bucles con var?

var tiene ámbito de función, así que todas las iteraciones comparten la misma variable. Los closures creados dentro del bucle apuntan a esa única variable, que cuando por fin se ejecutan ya tiene el valor final. La solución es usar let, que tiene ámbito de bloque y crea un binding nuevo en cada iteración.

Aprende a programar con Coddy

COMENZAR