Como sabemos por el capítulo Recolección de basura, el motor JavaScript mantiene un valor en la memoria mientras es "accesible" y potencialmente puede usarse.
Por ejemplo:
let john = { nombre: "John" }; // se puede acceder al objeto, john es la referencia al mismo // sobrescribe la referencia juan = nulo; // el objeto será eliminado de la memoria
Por lo general, las propiedades de un objeto o elementos de una matriz u otra estructura de datos se consideran accesibles y se mantienen en la memoria mientras esa estructura de datos está en la memoria.
Por ejemplo, si colocamos un objeto en una matriz, mientras la matriz esté viva, el objeto también estará vivo, incluso si no hay otras referencias a él.
Como esto:
let john = { nombre: "John" }; let matriz = [juan]; juan = nulo; // sobrescribe la referencia // el objeto al que john hizo referencia anteriormente se almacena dentro de la matriz // por lo tanto no será recolectado como basura // podemos obtenerlo como matriz[0]
De manera similar, si usamos un objeto como clave en un Map
normal, mientras el Map
exista, ese objeto también existirá. Ocupa memoria y es posible que no se recolecte basura.
Por ejemplo:
let john = { nombre: "John" }; dejar mapa = nuevo mapa(); mapa.set(juan, "..."); juan = nulo; // sobrescribe la referencia // john se almacena dentro del mapa, // podemos obtenerlo usando map.keys()
WeakMap
es fundamentalmente diferente en este aspecto. No impide la recolección de basura de objetos clave.
Veamos qué significa con ejemplos.
La primera diferencia entre Map
y WeakMap
es que las claves deben ser objetos, no valores primitivos:
let mapadébil = nuevo mapadébil(); dejar objeto = {}; débilMap.set(obj, "ok"); // funciona bien (clave de objeto) // no puedo usar una cadena como clave débilMap.set("prueba", "Ups"); // Error, porque "prueba" no es un objeto
Ahora, si usamos un objeto como clave y no hay otras referencias a ese objeto, se eliminará de la memoria (y del mapa) automáticamente.
let john = { nombre: "John" }; let mapadébil = nuevo mapadébil(); mapa débil.set(juan, "..."); juan = nulo; // sobrescribe la referencia // ¡john se elimina de la memoria!
Compárelo con el ejemplo Map
normal anterior. Ahora, si john
solo existe como clave de WeakMap
, se eliminará automáticamente del mapa (y de la memoria).
WeakMap
no admite iteraciones ni métodos keys()
, values()
, entries()
, por lo que no hay forma de obtener todas las claves o valores de él.
WeakMap
solo tiene los siguientes métodos:
weakMap.set(key, value)
weakMap.get(key)
weakMap.delete(key)
weakMap.has(key)
¿Por qué tal limitación? Eso es por razones técnicas. Si un objeto ha perdido todas las demás referencias (como john
en el código anterior), entonces se recolectará automáticamente como basura. Pero técnicamente no se especifica exactamente cuándo se realiza la limpieza .
El motor JavaScript decide eso. Puede optar por realizar la limpieza de la memoria inmediatamente o esperar y realizar la limpieza más tarde cuando se produzcan más eliminaciones. Entonces, técnicamente, no se conoce el recuento actual de elementos de un WeakMap
. Puede que el motor lo haya limpiado o no, o lo haya hecho parcialmente. Por ese motivo, no se admiten métodos que accedan a todas las claves/valores.
Ahora bien, ¿dónde necesitamos tal estructura de datos?
El principal campo de aplicación de WeakMap
es el almacenamiento de datos adicional .
Si estamos trabajando con un objeto que "pertenece" a otro código, tal vez incluso a una biblioteca de terceros, y nos gustaría almacenar algunos datos asociados con él, que solo deberían existir mientras el objeto esté vivo, entonces WeakMap
es exactamente lo que necesitamos. necesario.
Colocamos los datos en un WeakMap
, usando el objeto como clave, y cuando el objeto se recolecta como basura, esos datos también desaparecerán automáticamente.
débilMap.set(john, "documentos secretos"); // si john muere, los documentos secretos serán destruidos automáticamente
Veamos un ejemplo.
Por ejemplo, tenemos un código que mantiene un recuento de visitas de los usuarios. La información se almacena en un mapa: un objeto de usuario es la clave y el recuento de visitas es el valor. Cuando un usuario se va (su objeto se recolecta como basura), ya no queremos almacenar su recuento de visitas.
Aquí hay un ejemplo de una función de conteo con Map
:
// ? visitasCount.js let visitasCountMap = nuevo mapa(); // mapa: usuario => recuento de visitas //aumentar el recuento de visitas función contarUsuario(usuario) { let count = visitasCountMap.get(usuario) || 0; visitasCountMap.set(usuario, recuento + 1); }
Y aquí hay otra parte del código, tal vez otro archivo usándolo:
// ? principal.js let john = { nombre: "John" }; contarUsuario(juan); // cuenta sus visitas // luego john nos deja juan = nulo;
Ahora, el objeto john
debe recolectarse como basura, pero permanece en la memoria, ya que es una clave en visitsCountMap
.
Necesitamos limpiar visitsCountMap
cuando eliminamos usuarios; de lo contrario, crecerá en la memoria indefinidamente. Esta limpieza puede convertirse en una tarea tediosa en arquitecturas complejas.
Podemos evitarlo cambiando a WeakMap
en su lugar:
// ? visitasCount.js let visitasCountMap = new WeakMap(); // mapa débil: usuario => recuento de visitas //aumentar el recuento de visitas función contarUsuario(usuario) { let count = visitasCountMap.get(usuario) || 0; visitasCountMap.set(usuario, recuento + 1); }
Ahora no tenemos que limpiar visitsCountMap
. Después de que el objeto john
se vuelve inalcanzable, por todos los medios excepto como clave de WeakMap
, se elimina de la memoria, junto con la información de esa clave de WeakMap
.
Otro ejemplo común es el almacenamiento en caché. Podemos almacenar (“caché”) los resultados de una función, de modo que futuras llamadas al mismo objeto puedan reutilizarlos.
Para lograrlo, podemos usar Map
(escenario no óptimo):
// ? caché.js dejar caché = nuevo mapa(); // calcula y recuerda el resultado proceso de función (obj) { si (!cache.tiene(obj)) { let resultado = /* cálculos del resultado para */ obj; cache.set(obj, resultado); resultado de devolución; } devolver caché.get(obj); } // Ahora usamos proceso() en otro archivo: // ? principal.js let obj = {/* digamos que tenemos un objeto */}; let resultado1 = proceso(obj); // calculado // ...después, desde otro lugar del código... let resultado2 = proceso(obj); // resultado recordado tomado del caché // ...más tarde, cuando el objeto ya no sea necesario: objeto = nulo; alerta(caché.tamaño); // 1 (¡Ay! ¡El objeto todavía está en caché, ocupando memoria!)
Para múltiples llamadas de process(obj)
con el mismo objeto, solo calcula el resultado la primera vez y luego simplemente lo toma del cache
. La desventaja es que necesitamos limpiar cache
cuando el objeto ya no es necesario.
Si reemplazamos Map
con WeakMap
, este problema desaparece. El resultado almacenado en caché se eliminará de la memoria automáticamente después de que se recolecte la basura del objeto.
// ? caché.js dejar caché = nuevo WeakMap(); // calcula y recuerda el resultado proceso de función (obj) { si (!cache.tiene(obj)) { let result = /* calcular el resultado para */ obj; cache.set(obj, resultado); resultado de devolución; } devolver caché.get(obj); } // ? principal.js let obj = {/* algún objeto */}; let resultado1 = proceso(obj); let resultado2 = proceso(obj); // ...más tarde, cuando el objeto ya no sea necesario: objeto = nulo; // No se puede obtener cache.size, ya que es un WeakMap, // pero es 0 o pronto será 0 // Cuando se recolecta basura en obj, los datos almacenados en caché también se eliminarán
WeakSet
se comporta de manera similar:
Es análogo a Set
, pero solo podemos agregar objetos a WeakSet
(no primitivos).
Un objeto existe en el conjunto mientras que es accesible desde otro lugar.
Al igual que Set
, admite add
, has
y delete
, pero no size
, keys()
y no iteraciones.
Al ser “débil”, también sirve como almacenamiento adicional. Pero no por datos arbitrarios, sino por hechos de “sí o no”. Una membresía en WeakSet
puede significar algo sobre el objeto.
Por ejemplo, podemos agregar usuarios a WeakSet
para realizar un seguimiento de quienes visitaron nuestro sitio:
dejar conjunto visitado = nuevo conjunto débil(); let john = { nombre: "John" }; let pete = { nombre: "Pete" }; let mary = { nombre: "María" }; visitadoSet.add(juan); // Juan nos visitó visitadoSet.add(pete); // Entonces Pete visitadoSet.add(juan); // Juan otra vez // El conjunto visitado tiene 2 usuarios ahora // comprobar si John visitó? alerta(visitadoSet.has(juan)); // verdadero // comprobar si María visitó? alerta(visitadoSet.has(maría)); // FALSO juan = nulo; // el conjunto visitado se limpiará automáticamente
La limitación más notable de WeakMap
y WeakSet
es la ausencia de iteraciones y la imposibilidad de obtener todo el contenido actual. Esto puede parecer inconveniente, pero no impide que WeakMap/WeakSet
haga su trabajo principal: ser un almacenamiento "adicional" de datos para objetos que se almacenan/administran en otro lugar.
WeakMap
es una colección similar a Map
que permite solo objetos como claves y los elimina junto con el valor asociado una vez que se vuelven inaccesibles por otros medios.
WeakSet
es una colección tipo Set
que almacena solo objetos y los elimina una vez que se vuelven inaccesibles por otros medios.
Sus principales ventajas son que tienen una referencia débil a los objetos, por lo que el recolector de basura puede eliminarlos fácilmente.
Esto tiene el costo de no tener soporte para clear
, size
, keys
, values
...
WeakMap
y WeakSet
se utilizan como estructuras de datos "secundarias" además del almacenamiento de objetos "primario". Una vez que el objeto se elimina del almacenamiento principal, si solo se encuentra como clave de WeakMap
o en un WeakSet
, se limpiará automáticamente.
importancia: 5
Hay una variedad de mensajes:
dejar mensajes = [ {texto: "Hola", de: "Juan"}, {texto: "¿Cómo va?", de: "John"}, {texto: "Hasta pronto", de: "Alice"} ];
Tu código puede acceder a él, pero los mensajes son administrados por el código de otra persona. Se agregan nuevos mensajes, los antiguos se eliminan periódicamente mediante ese código y no sabes los momentos exactos en que sucede.
Ahora bien, ¿qué estructura de datos podrías utilizar para almacenar información sobre si el mensaje “ha sido leído”? La estructura debe ser adecuada para dar la respuesta “¿fue leído?” para el objeto de mensaje dado.
PD: Cuando un mensaje se elimina de messages
, también debería desaparecer de su estructura.
PPS No deberíamos modificar los objetos de mensaje, agregarles nuestras propiedades. Como están administrados por el código de otra persona, eso puede tener malas consecuencias.
Almacenemos los mensajes leídos en WeakSet
:
dejar mensajes = [ {texto: "Hola", de: "Juan"}, {texto: "¿Cómo va?", de: "John"}, {texto: "Hasta pronto", de: "Alice"} ]; let readMessages = new WeakSet(); // se han leído dos mensajes readMessages.add(mensajes[0]); readMessages.add(mensajes[1]); // readMessages tiene 2 elementos // ...¡leamos el primer mensaje otra vez! readMessages.add(mensajes[0]); // readMessages todavía tiene 2 elementos únicos // respuesta: ¿se leyó el mensaje[0]? alert("Leer mensaje 0: " + readMessages.has(mensajes[0])); // verdadero mensajes.shift(); // ahora readMessages tiene 1 elemento (técnicamente, la memoria se puede limpiar más tarde)
WeakSet
permite almacenar un conjunto de mensajes y comprobar fácilmente la existencia de un mensaje en él.
Se limpia solo automáticamente. La desventaja es que no podemos iterar sobre él, no podemos obtener "todos los mensajes leídos" directamente. Pero podemos hacerlo iterando sobre todos los mensajes y filtrando los que están en el conjunto.
Otra solución diferente podría ser agregar una propiedad como message.isRead=true
a un mensaje después de leerlo. Como los objetos de mensajes son administrados por otro código, esto generalmente no se recomienda, pero podemos usar una propiedad simbólica para evitar conflictos.
Como esto:
// la propiedad simbólica sólo la conoce nuestro código let isRead = Símbolo("isRead"); mensajes[0][isRead] = verdadero;
Ahora el código de terceros probablemente no verá nuestra propiedad adicional.
Aunque los símbolos permiten reducir la probabilidad de problemas, usar WeakSet
es mejor desde el punto de vista arquitectónico.
importancia: 5
Hay una serie de mensajes como en la tarea anterior. La situación es similar.
dejar mensajes = [ {texto: "Hola", de: "Juan"}, {texto: "¿Cómo va?", de: "John"}, {texto: "Hasta pronto", de: "Alice"} ];
La pregunta ahora es: ¿qué estructura de datos sugeriría para almacenar la información: "¿cuándo se leyó el mensaje?".
En la tarea anterior sólo necesitábamos almacenar el dato “sí/no”. Ahora necesitamos almacenar la fecha, y solo debe permanecer en la memoria hasta que el mensaje sea recolectado como basura.
PS Dates se puede almacenar como objetos de la clase Date
incorporada, que cubriremos más adelante.
Para almacenar una fecha, podemos usar WeakMap
:
dejar mensajes = [ {texto: "Hola", de: "Juan"}, {texto: "¿Cómo va?", de: "John"}, {texto: "Hasta pronto", de: "Alice"} ]; let readMap = nuevo Mapa Débil(); readMap.set(mensajes[0], nueva fecha(2017, 1, 1)); // Objeto de fecha que estudiaremos más adelante