¿Closures en JS?

Closures en javaScript, identificarlos y utilizarlos en tu código, aprende como es que funcionan.

Luis Angel Mendoza Lucio

¿Habrás utilizado antes los closures sin saberlo? Aprende a identificarlos y a sacarles provecho en tu código.

Primero podemos empezar por saber lo que es una función, supongamos que tenemos unas líneas de código:

const a = 1;
const b = 2;

const sum = a + b;

console.log(sum); // 3

Pero por alguna razón queremos utilizar esas mismas líneas en otra parte. Surge la necesidad de reutilizar código y las funciones nos ayudan con eso, podemos encapsular código dentro de una función y usar dicha función cuantas veces queramos.

function sum(a, b) {
	console.log(a + b);
}

sum(1, 9); // 10
sum(1, 1); // 2

Ya visto lo que es una función, también debemos saber que en JavaScript las funciones son tratadas como cualquier otro valor, podemos asignarlas a una variable, incluso pasarlas como parámetros a otras funciones o hacer que una función retorne otra función 👁️ ¡Ojo aquí!

Los closures son funciones anidadas que recuerdan el scope en el cual fueron creadas, aunque se ejecuten en otro contexto.

Aquí viene a juego lo que se comentó antes, eso de que una función puede retornar otra función, pues la magia está en que esa función retornada podrá en todo momento recordar aquellas variables y funciones que estaban en el scope donde fue creada y referenciar a ella. Siempre es mejor verlo con un ejemplo sencillo:

// Haremos la famosa función contadora
function makeCounter() {
	let count = 0;
    
    return function increment() {
    	count += 1;
      	console.log(count);
    }
}

const counter1 = makeCounter();
console.log(counter1); // función increment()
// vemos que counter1 es en realidad 
// una función (la función increment) y es nuestro closure
// para obtener el resultado debemos invocar la funcion counter1

counter1() // 1;
counter1() // 2;
counter1() // 3;
counter1() // 4;

Como se puede observar, se mantiene viva la variable count que fue creada en la función makeCounter, incluso después de que makeCounter acabara su ejecución y saliera de la pila de ejecución.

Describamos de forma simple que es lo que está pasando:

  1. Se crea la función makeCounter
  2. Dentro de la función makeCounter se crea una variable llamada count y una función llamada increment
  3. La función increment hace uso de la variable count perteneciente al scope de la función makeCounter
  4. La función makeCounter retorna la función increment

Ahora debemos saber qué pasa cuando se ejecuta una función en JavaScript. Primero se crea un contexto de ejecución, que es el código que se está ejecutando actualmente, todo contexto de ejecución consta de 2 fases:

  1. Fase de creación
  2. Fase de ejecución

En la fase de creación se carga la información necesaria para que se pueda después ejecutar la función, entre esa información está el nombre del archivo donde está la función, el valor que tendrá this, la próxima instrucción a ejecutar, además se crea arguments y se crea un entorno léxico. Un entorno léxico se puede ver como una tabla clave-valor, donde estarán las variables dentro de la función y su respectivo valor inicial, en el caso de la función makeCounter en su entorno léxico tendrá la variable count que como está declarada con let tendrá undefined como valor inicial y algo muy importante que entender, también se crea un enlace entre su entorno léxico y el entorno léxico exterior, que vendría siendo el entorno en donde fue creada la función makeCounter, para este ejemplo es el entorno léxico global (el primero que se crea).

En la fase de ejecución se empieza a ejecutar línea a línea la función, asignando los valores a las variables y se va actualizando el entorno léxico. En el caso de la función makeCounter, en esta fase a la variable count se le asigna el valor de 0, luego se ejecuta la instrucción:

...
return function increment() {
	count += 1;
	console.log(count);
}

Como es una función, JavaScript crea una instancia de esa función en memoria y la vincula con el entorno léxico exterior, es decir, con el entorno léxico de la función makeCounter donde fue creada.

...
const counter1 = makeCounter();
...

En la línea del código anterior se está asignando a la variable counter1 el closure y ya sabemos que ese closure tiene un vínculo con el entorno léxico de la función makeCounter.

...
counter1();
...

Al ejecutar el closure sucede lo que ya habíamos descrito antes, se crea un nuevo entorno de ejecución y luego el entorno léxico, en este caso no hay creación de variables dentro del closure entonces su entorno léxico está vacío, pero ya tiene vinculado el entorno léxico exterior. Luego, al llegar a su fase de ejecución...

...
// esta función es el closure que se retornó
return function increment() {
	count += 1;
	console.log(count);
}

Se incrementa la variable count, pero como no existe esa variable en el entorno léxico de la función closure, la busca en el entorno léxico exterior que tiene vinculado, donde si encuentra la variable y puede seguir utilizándola, entonces se incrementa la variable count en su respectivo entorno léxico y luego se pasa a la siguiente línea donde solo imprime el valor de la variable.

¿Se entendió esto? La variable count no se encuentra en el entorno léxico del closure, sino en otro entorno léxico, en un espacio en memoria, existiendo y actualizándose, es por eso que al llamar de nuevo a la función counter1(), que es nuestro closure, se repite el mismo procedimiento: busca la variable count en su entorno lexico, al no encontrarla, la busca en el entorno lexico superior donde la encuentra con el previo valor de 1, la actualiza y así sigue repitiéndose el flujo mientras se siga invocando la función.

...
const counter2 = makeCounter();

counter2();

Si sabes lo que pasará en el código de arriba, entonces puedes decir que ya entendiste como funcionan los closure, los contextos de ejecución y los entornos léxicos
...
Así es, al ejecutarse la función makeCounter se crea un nuevo contexto de ejecución y un nuevo entorno léxico, separado del que se había creado antes, es por eso que al llamar a la función counter2() iniciará su conteo de nuevo desde 1.

¿Para qué podemos usar los closures?

Vamos a dar 1 ejemplo más para utilizar los closures:
Crear diferentes listas de música

function makePlaylist() {
  const list = new Map();
  
  function add(music) {
    if (list.has(music.id)) return;
    list.set(music.id, music);
  }
  
  function getPlaylist() {
    return list;
  }
  
  return {
    add: add,
    getPlaylist: getPlaylist
  };
}

const rap = makePlaylist();
const rock = makePlaylist();

rap.add({id: 1, name: 'Rap good'});
rap.add({id: 2, name: 'Lose yourself'});

rock.add({id: 2, name: 'Imagine'});

const myRapMusic = rap.getPlaylist();
for (const [key, value] of myRapMusic) {
  console.log(value);
}
// Rap good
// Lose yourself

const myRockMusic = rock.getPlaylist();
for (const [key, value] of myRockMusic) {
  console.log(value);
}
// Imagine

Ahora podemos ver que dentro de la función makePlaylist tenemos 2 closures: add y getPlaylist, ambas las estamos retornando en un objeto para luego ser utilizadas. Además, ahora tenemos una forma de mantener protegidas la variable list, pues será imposible alterarla de otra manera que no sea con la función add. Otro punto para los closures. Esta es la forma que se tenía en JavaScript para tener variables privadas.

Pues ahora podemos hacer tantas listas como queramos, siempre se mantendrán en un entorno léxico separado.

Conclusiones

Como vimos, el concepto de closure como tal no es complejo de entender, pero si debemos conocer tanto el contexto de ejecución como el entorno léxico y puede ser que entre tanto concepto se haga un poco tedioso, pero ahora siempre que veas una función dentro de otra podrás pensar en los closures, ya dependerá del programador y del problema a resolver el sí usaras o no algún closure. Ya habiendo entendido lo que son los closure, como identificarlos y usarlos, puedes buscar otros temas, como la currificación o algunos patrones de diseño, donde utilizar closures es indispensable.

ProgramaciónShorts

Luis Angel Mendoza Lucio

Líder de proyectos de frontend, aunque es una persona reservada, le encanta compartir su conocimiento y experiencia con otros, también es ávido lector, y disfruta investigar sobre la historia.

Comentarios