¿Closures en JS?
Closures en javaScript, identificarlos y utilizarlos en tu código, aprende como es que funcionan.
¿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:
- Se crea la función
makeCounter
- Dentro de la función
makeCounter
se crea una variable llamadacount
y una función llamadaincrement
- La función
increment
hace uso de la variablecount
perteneciente al scope de la funciónmakeCounter
- La función
makeCounter
retorna la funciónincrement
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:
- Fase de creación
- 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.
Krative Digital Boletin informativo
Únase al boletín para recibir las últimas actualizaciones en su bandeja de entrada.