Todo proceso en computación necesita de cierto espacio en memoria para ejecutarse.
Cuando tu ejecutas un programa, cada variable que declaras, cada función, cada dato que manejas, sea un objeto o sea un valor primitivo, todo ocupa memoria.
Pero creo que es obvio decir que no todo lo que se se guarda en memoria se queda para siempre en tu computadora. Si ese fuera el caso, la capacidad de nuestros ordenadores se acabaría en un abrir y cerrar de ojos. ¿Cómo es que Javascript sabe cuales cosas pueden dejar de ocupar memoria y cuáles no? ¿Y como libera la memoria una vez decide hacerlo?
Bueno, primero lo primero. Para decidir si debe liberar memoria o no, Javascript utiliza un concepto con el que ya deberías estar muy familiarizado. Scope. (Y si no lo estas. ¿Pues qué estas esperando? -> Lexical Scope en Javascript)
Como estoy seguro que ya sabes el scope de una variable es el alcance que tiene la misma en el código. Javascript solo mantiene las referencias a las variables que se encuentran dentro del scope que se esta procesando. Las referencias que no cumplan con este requisito son abandonadas para que sean recolectadas por el Recolector de Basura o GC (Garbage Collector).
Pero como siempre un ejemplo dice más que mil palabras:
Referencia simple
Empecemos con una referencia a una variable dentro del global scope:
let game = {
title: "Final Fantasy"
}
En este caso la variable game
hace referencia al objeto {title: "Final Fantasy"}
. El global scope es el scope principal por lo que nada de lo que este referenciado aquí será recolectado jamás por el GC.
El GC entra en acción cuando decidimos deshacernos de la referencia que existía al objeto anterior.
game = null
Si a la variable game
se le asigna otro valor (como null), en ese momento el objeto {title: "Final Fantasy"}
se vuelve inaccessible. No hay manera de que el código recupere la referencia a él y por ende es candidato para que el GC lo recolecte y libere el espacio en memoria que ocupaba originalmente.
Referencias Múltiples
Volvamos al escenario original, pero supongamos que ahora antes de asignarle el valor de null a game
, se introduce la variable en un arreglo
let game = {
title: "Final Fantasy"
}
let gamesList = [game]
En este caso tenemos una variable llamada gameList
que hace referencia a un arreglo en memoria. El arreglo solo tiene un elemento y ese elemento hace referencia al mismo objeto que game
.
¿Cómo se ve el diagrama cuando game
pierde la referencia al objeto?
game = null
¿Lo recolectará el Garbage Collector?
Probablemente las flechitas te dieron una buena pista pero la es NO. La razón es simple. Ya que a pesar de que ya no se puede acceder a ese objeto a través de game
, todavía se puede acceder a él a través del arreglo de gameList
.
Referencias en local scope
En los ejemplos anteriores no nos hemos metido realmente a escenarios en los que varía el scope. Tenerlo en cuenta es ligeramente más complejo, no por el código sino porque se tienen que tener mas cosas en mente:
let game1 = {
title: "Final Fantasy",
sequel: game2,
favorite: false,
releaseDate: "1987-12-18"
}
let game2 = {
title: "Final Fantasy X",
prequel: game1,
favorite: true,
releaseDate: "2001-07-19"
}
let game3 = {
title: "Devil May Cry 5",
favorite: true,
releaseDate: "2019-03-08"
}
let gamesList = [game1, game2, game3]
function getFavoriteGames(gamesList) {
const connectionString = 'dbConnectionString'
const favoriteGames = gamesList.filter(game => game.favorite)
return favoriteGames
}
El diagrama creció bastante pero si te fijas los conceptos que tenemos son los mismos que ya viste.
Por el momento seguimos solo lidiando con el global scope, pero las referencias ahora estan presentes prácticamente entre todos los objetos.
game1
, game2
y game3
hacen referencia cada uno a un objeto diferente, cada uno representando un videojuego.
El objeto de la variable game1
(el juego de Final Fantasy) hace referencia al objeto de “Final Fantasy X” a través de la propiedad de sequel
.
...
let game1 = {
title: "Final Fantasy",
sequel: game2,
favorite: false,
releaseDate: "1987-12-18"
}
...
Lo mismo pasa a la inversa. “Final Fantasy X” hace referencia a “Final Fantasy” a trav[es de la propiedad de prequel
.
...
let game2 = {
title: "Final Fantasy X",
prequel: game1,
favorite: true,
releaseDate: "2001-07-19"
}
...
getFavoriteGames
es el nombre que se le puso a la función y a su vez hace referencia al código que se ejecutará al momento de llamarla.
gamesList
es un arreglo de 3 elementos, en el que cada elemento hace referencia a uno de los videojuegos anteriores.
Las cosas cambian cuando se manda a llamar la función de getFavoriteGames
.
Agrega esta línea al final del script para analizar que pasa.
let favorites = getFavoriteGames(gamesList)
Cuando se ejecuta la función las referencias en memoria se actualizan:
Se crea un local scope para la función getFavoriteGames
y dentro del mismo se crean nuevas referencias.
gamesList
es el parámetro de entrada que recibe la función. Al pasarle el valor de la variable global de gameList
, la variable local del mismo nombre copia las mismas referencias.
connectionString
contiene solo una cadena de texto. En un programa real, tendría contenido mas interesante para conectarse a una base de datos real, pero de todos modos sería texto. Así que usa tu imaginación
favoriteGames
es un arreglo creado por esta línea
...
const favoriteGames = gamesList.filter(game => game.favorite)
...
Lo único que hace es sacar todos los objetos referenciados en el arreglo de gameList
que tienen la propiedad favorite
como true
. De los 3 objetos, sólo 2 cumplen con este requisito por lo que favoriteGames
solo guarda la referencia a esos mismos.
¿Cuánto tiempo van a vivir en memoria todas estas referencias?
De seguro ya te lo imaginas pero en el momento en el que termina la ejecución de la función se perderán prácticamente todas.
La variable local de gamesList
se elimina pero los objetos a los que hace referencia no son afectados porque las variables globales siguen apuntando a las mismas.
El valor de connectionString
se vuelve inaccesible y por lot tanto ese valor si se libera de memoria.
Por último favoriteGames
se elimina pero su valor se queda en memoria porque se le reasigna a la variable global favorites
.
*Dicho sea de paso, si no hubieramos asignado el valor retornado de la función de getFavoriteGames
entonces no se hubiera preservado nada de lo que se hizo dentro de la función.
Solo agregamos un ejemplo de lo que pasa al ejecutar una función, pero como puedes ver las relación entre referencias ya empieza a enmarañarse.
¿Que pasaría si borrararamos las referencias de un par de variables?
game2 = null
gameList = null
Intenta imaginartelo antes de ver la respuesta.
¿Qué pasaría con los objetos a los que hace referencia tanto game2
como gameList
?
…
¿Listo?
Al quitar la referencia de game2
al objeto del juego de “Final Fantasy X” no ocurre nada porque todavía hay 3 lugares que guardan referencias al mismo: la propiedad sequel
del objeto de “Final Fantasy”, el arreglo de gameList
y el arreglo de favorites
.
Al quitar la referencia de gameList
perdemos 3 referencias a 3 objetos pero todos siguen siendo accesibles desde otras propiedades. Sin embargo el espacio dispuesto para el arreglo ya no se esta usando y el GC entra en acción para recolectar esa basura.
El comportamiento de GC es una de las razones por las que se recomienda evitar mantener referencias a variables no utilizadas o a deshacerse de ellas lo mas rapido posible.
¿No es tan dificíl cierto? Como desarrolladores de JS es muy raro que tengas que lidiar con problemas de gestión de memoria pero el saber com funciona todo y el seguir estas buenas prácticas te ayudará a evitar la mayoría de problemas de optimización que puedan surgir.