¿Cuáles son los usos reales de ES6 WeakMap?

397

¿Cuáles son los usos reales de la WeakMapestructura de datos introducida en ECMAScript 6?

Dado que una clave de un mapa débil crea una referencia fuerte a su valor correspondiente, asegurando que un valor que se ha insertado en un mapa débil nunca desaparecerá mientras su clave siga viva, no se puede usar para tablas de notas, cachés o cualquier otra cosa para la que normalmente usaría referencias débiles, mapas con valores débiles, etc.

Me parece que esto:

weakmap.set(key, value);

... es solo una forma indirecta de decir esto:

key.value = value;

¿Qué casos de uso concretos me estoy perdiendo?

valderman
fuente
35
Caso de uso del mundo real: almacene datos personalizados para nodos DOM.
Felix Kling
Todos los casos de uso que menciona para referencias débiles también son muy importantes. Son mucho más difíciles de agregar al lenguaje ya que introducen el no determinismo. Mark Miller y otros han trabajado mucho en referencias débiles y creo que eventualmente vendrán. Finalmente
Benjamin Gruenbaum
2
WeakMaps se puede usar para detectar pérdidas de memoria: stevehanov.ca/blog/?id=148
theWebalyst

Respuestas:

513

Fundamentalmente

WeakMaps proporciona una forma de extender objetos desde el exterior sin interferir con la recolección de basura. Siempre que desee extender un objeto pero no pueda porque está sellado, o desde una fuente externa, se puede aplicar un WeakMap.

Un WeakMap es un mapa (diccionario) donde las claves son débiles, es decir, si se pierden todas las referencias a la clave y no hay más referencias al valor, el valor se puede recolectar basura. Vamos a mostrar esto primero a través de ejemplos, luego explicarlo un poco y finalmente terminar con un uso real.

Digamos que estoy usando una API que me da un cierto objeto:

var obj = getObjectFromLibrary();

Ahora, tengo un método que usa el objeto:

function useObj(obj){
   doSomethingWith(obj);
}

Quiero hacer un seguimiento de cuántas veces se llamó al método con un determinado objeto e informar si sucede más de N veces. Ingenuamente uno pensaría usar un Mapa:

var map = new Map(); // maps can have object keys
function useObj(obj){
    doSomethingWith(obj);
    var called = map.get(obj) || 0;
    called++; // called one more time
    if(called > 10) report(); // Report called more than 10 times
    map.set(obj, called);
}

Esto funciona, pero tiene una pérdida de memoria: ahora hacemos un seguimiento de cada objeto de biblioteca que se pasa a la función que evita que los objetos de la biblioteca se recojan basura. En cambio, podemos usar un WeakMap:

var map = new WeakMap(); // create a weak map
function useObj(obj){
    doSomethingWith(obj);
    var called = map.get(obj) || 0;
    called++; // called one more time
    if(called > 10) report(); // Report called more than 10 times
    map.set(obj, called);
}

Y la pérdida de memoria se ha ido.

Casos de uso

Algunos casos de uso que de otro modo causarían una pérdida de memoria y están habilitados por WeakMaps incluyen:

  • Mantener datos privados sobre un objeto específico y solo dar acceso a ellos a personas con una referencia al Mapa. Un enfoque más ad-hoc viene con la propuesta de símbolos privados, pero eso es mucho tiempo a partir de ahora.
  • Mantener datos sobre los objetos de la biblioteca sin cambiarlos o incurrir en gastos generales.
  • Mantener datos sobre un pequeño conjunto de objetos donde existen muchos objetos del tipo para no incurrir en problemas con las clases ocultas que usan los motores JS para objetos del mismo tipo.
  • Mantener datos sobre objetos host como nodos DOM en el navegador.
  • Agregar una capacidad a un objeto desde el exterior (como el ejemplo del emisor de eventos en la otra respuesta).

Veamos un uso real

Se puede usar para extender un objeto desde el exterior. Pongamos un ejemplo práctico (adaptado, más o menos real, para destacar) del mundo real de Node.js.

Digamos que usted es Node.js y tiene Promiseobjetos, ahora desea realizar un seguimiento de todas las promesas rechazadas actualmente; sin embargo, no desea evitar que se recojan basura en caso de que no existan referencias a ellos.

Ahora, no desea agregar propiedades a objetos nativos por razones obvias, por lo que está atascado. Si mantiene referencias a las promesas, está causando una pérdida de memoria ya que no se puede recolectar basura. Si no mantiene referencias, no puede guardar información adicional sobre promesas individuales. Cualquier esquema que implique guardar la identificación de una promesa inherentemente significa que necesita una referencia a ella.

Ingrese WeakMaps

WeakMaps significa que las teclas son débiles. No hay formas de enumerar un mapa débil o de obtener todos sus valores. En un mapa débil, puede almacenar los datos basados ​​en una clave y cuando la clave se recolecta basura, también lo hacen los valores.

Esto significa que, dada una promesa, puede almacenar el estado al respecto, y ese objeto aún puede ser recolectado. Más adelante, si obtiene una referencia a un objeto, puede verificar si tiene algún estado relacionado con él e informarlo.

Esto fue utilizado para implementar ganchos de rechazo no manejados por Petka Antonov como este :

process.on('unhandledRejection', function(reason, p) {
    console.log("Unhandled Rejection at: Promise ", p, " reason: ", reason);
    // application specific logging, throwing an error, or other logic here
});

Mantenemos información sobre las promesas en un mapa y podemos saber cuándo se manejó una promesa rechazada.

Benjamin Gruenbaum
fuente
8
¡Hola! ¿Podría decirme qué parte del código de ejemplo causa pérdida de memoria?
ltamajs
15
@ ltamajs4 seguro, en el useObjejemplo usando a Mapy no a WeakMapusamos el objeto pasado como clave de mapa. El objeto nunca se elimina del mapa (ya que no sabríamos cuándo hacerlo), por lo que siempre hay una referencia a él y nunca se puede recolectar basura. En el ejemplo de WeakMap, tan pronto como todas las demás referencias al objeto hayan desaparecido, el objeto se puede borrar de WeakMap. Si todavía no está seguro de lo que quiero decir, hágamelo saber
Benjamin Gruenbaum
@Benjamin, necesitamos distinguir entre la necesidad de una memoria caché sensible a la memoria y la necesidad de una tupla data_object. No mezcle estos dos requisitos separados. Su calledejemplo está mejor escrito usando jsfiddle.net/f2efbm7z y no demuestra el uso de un mapa débil. De hecho, se puede escribir mejor en un total de 6 formas, que enumeraré a continuación.
Pacerier
Básicamente, el propósito del mapa débil es un caché sensible a la memoria. Si bien se puede usar para extender objetos desde el exterior, ese es un truco pésimo no utilizado y definitivamente no es su propósito adecuado .
Pacerier
1
Si desea mantener el vínculo entre una promesa y la cantidad de veces que se ha manejado / rechazado, use el símbolo 1) ; p[key_symbol] = data. o 2) nombres únicos; p.__key = data. o 3) ámbito privado; (()=>{let data; p.Key = _=>data=_;})(). o 4) proxy con 1 o 2 o 3. o 5) reemplazar / extender la clase Promise con 1 o 2 o 3. o 6) reemplazar / extender la clase Promise con una tupla de miembros necesarios. - En cualquier caso, no se necesita un mapa débil a menos que necesite una memoria caché sensible a la memoria.
Pacerier
48

Esta respuesta parece estar sesgada e inutilizable en un escenario del mundo real. Léalo como está y no lo considere como una opción real para otra cosa que no sea la experimentación.

Un caso de uso podría ser usarlo como un diccionario para los oyentes, tengo un compañero de trabajo que hizo eso. Es muy útil porque cualquier oyente está directamente dirigido con esta forma de hacer las cosas. Adiós listener.on.

Pero desde un punto de vista más abstracto, WeakMapes especialmente poderoso para desmaterializar el acceso a básicamente cualquier cosa, no necesita un espacio de nombres para aislar a sus miembros ya que ya está implícito en la naturaleza de esta estructura. Estoy bastante seguro de que podría hacer algunas mejoras importantes en la memoria al reemplazar las claves de objeto redundantes incómodas (aunque la deconstrucción hace el trabajo por usted).


Antes de leer lo que sigue

Ahora me doy cuenta de que mi énfasis no es exactamente la mejor manera de abordar el problema y, como señaló Benjamin Gruenbaum (vea su respuesta, si aún no está por encima de la mía: p), este problema no podría haberse resuelto con regularidad Map, ya que se habría filtrado, por lo tanto, la principal fortaleza WeakMapes que no interfiere con la recolección de basura dado que no mantienen una referencia.


Aquí está el código real de mi compañero de trabajo (gracias a él por compartir)

Fuente completa aquí , se trata de la gestión de los oyentes que mencioné anteriormente (también puede consultar las especificaciones )

var listenableMap = new WeakMap();


export function getListenable (object) {
    if (!listenableMap.has(object)) {
        listenableMap.set(object, {});
    }

    return listenableMap.get(object);
}


export function getListeners (object, identifier) {
    var listenable = getListenable(object);
    listenable[identifier] = listenable[identifier] || [];

    return listenable[identifier];
}


export function on (object, identifier, listener) {
    var listeners = getListeners(object, identifier);

    listeners.push(listener);
}


export function removeListener (object, identifier, listener) {
    var listeners = getListeners(object, identifier);

    var index = listeners.indexOf(listener);
    if(index !== -1) {
        listeners.splice(index, 1);
    }
}


export function emit (object, identifier, ...args) {
    var listeners = getListeners(object, identifier);

    for (var listener of listeners) {
        listener.apply(object, args);
    }
}
axelduch
fuente
2
No entiendo cómo usarías esto. Causaría que lo observable colapsase junto con los eventos vinculados a él cuando ya no se haga referencia a él. El problema que suelo tener es cuando ya no se hace referencia al Observador. Creo que la solución aquí solo resolvió la mitad del problema. No creo que pueda resolver el problema del observador con WeakMap ya que no es iterable.
jgmjgm
1
Los oyentes de eventos de doble búfer pueden ser rápidos en otros idiomas, pero en este caso es simplemente esotérico y lento. Esos son mis tres centavos.
Jack Giffin
@axelduch, ¡Guau, este mito del oyente ha sido promocionado hasta la comunidad Javascript, obteniendo 40 votos a favor! Para comprender por qué esta respuesta es completamente incorrecta , vea los comentarios en stackoverflow.com/a/156618/632951
Pacerier el
1
@Pacerier actualizó la respuesta, gracias por los comentarios
axelduch
1
@axelduch, sí, hay una referencia desde allí también.
Pacerier
18

WeakMap funciona bien para encapsular y ocultar información

WeakMapsolo está disponible para ES6 y superior. A WeakMapes una colección de pares de clave y valor donde la clave debe ser un objeto. En el siguiente ejemplo, construimos un WeakMapcon dos elementos:

var map = new WeakMap();
var pavloHero = {first: "Pavlo", last: "Hero"};
var gabrielFranco = {first: "Gabriel", last: "Franco"};
map.set(pavloHero, "This is Hero");
map.set(gabrielFranco, "This is Franco");
console.log(map.get(pavloHero));//This is Hero

Utilizamos el set()método para definir una asociación entre un objeto y otro elemento (una cadena en nuestro caso). Utilizamos el get()método para recuperar el elemento asociado con un objeto. El aspecto interesante de la WeakMaps es el hecho de que tiene una referencia débil a la clave dentro del mapa. Una referencia débil significa que si el objeto se destruye, el recolector de basura eliminará toda la entrada del WeakMap, liberando así la memoria.

var TheatreSeats = (function() {
  var priv = new WeakMap();
  var _ = function(instance) {
    return priv.get(instance);
  };

  return (function() {
      function TheatreSeatsConstructor() {
        var privateMembers = {
          seats: []
        };
        priv.set(this, privateMembers);
        this.maxSize = 10;
      }
      TheatreSeatsConstructor.prototype.placePerson = function(person) {
        _(this).seats.push(person);
      };
      TheatreSeatsConstructor.prototype.countOccupiedSeats = function() {
        return _(this).seats.length;
      };
      TheatreSeatsConstructor.prototype.isSoldOut = function() {
        return _(this).seats.length >= this.maxSize;
      };
      TheatreSeatsConstructor.prototype.countFreeSeats = function() {
        return this.maxSize - _(this).seats.length;
      };
      return TheatreSeatsConstructor;
    }());
})()
Michael Horojanski
fuente
44
Re "mapa débil funciona bien para encapsular y ocultar información". Solo porque puedas, no significa que debas. Javascript tiene formas predeterminadas de encapsular y ocultar información incluso antes de que se inventara el mapa débil. Como en este momento, hay literalmente 6 formas de hacerlo . Usar el mapa débil para hacer la encapsulación es una cara fea fea.
Pacerier
12

𝗠𝗲𝘁𝗮𝗱𝗮𝘁𝗮

Los mapas débiles se pueden usar para almacenar metadatos sobre elementos DOM sin interferir con la recolección de basura o hacer que los compañeros de trabajo se enojen con su código. Por ejemplo, podría usarlos para indexar numéricamente todos los elementos de una página web.

𝗪𝗶𝘁𝗵𝗼𝘂𝘁 𝗪𝗲𝗮𝗸𝗠𝗮𝗽𝘀 𝗼𝗿 𝗪𝗲𝗮𝗸𝗦𝗲𝘁𝘀:

var elements = document.getElementsByTagName('*'),
  i = -1, len = elements.length;

while (++i !== len) {
  // Production code written this poorly makes me want to cry:
  elements[i].lookupindex = i;
  elements[i].elementref = [];
  elements[i].elementref.push( elements[(i * i) % len] );
}

// Then, you can access the lookupindex's
// For those of you new to javascirpt, I hope the comments below help explain 
// how the ternary operator (?:) works like an inline if-statement
document.write(document.body.lookupindex + '<br />' + (
    (document.body.elementref.indexOf(document.currentScript) !== -1)
    ? // if(document.body.elementref.indexOf(document.currentScript) !== -1){
    "true"
    : // } else {
    "false"
  )   // }
);

𝗨𝘀𝗶𝗻𝗴 𝗪𝗲𝗮𝗸𝗠𝗮𝗽𝘀 𝗮𝗻𝗱 𝗪𝗲𝗮𝗸𝗦𝗲𝘁𝘀:

var DOMref = new WeakMap(),
  __DOMref_value = Array,
  __DOMref_lookupindex = 0,
  __DOMref_otherelement = 1,
  elements = document.getElementsByTagName('*'),
  i = -1, len = elements.length, cur;

while (++i !== len) {
  // Production code written this greatly makes me want to 😊:
  cur = DOMref.get(elements[i]);
  if (cur === undefined)
    DOMref.set(elements[i], cur = new __DOMref_value)

  cur[__DOMref_lookupindex] = i;
  cur[__DOMref_otherelement] = new WeakSet();
  cur[__DOMref_otherelement].add( elements[(i * i) % len] );
}

// Then, you can access the lookupindex's
cur = DOMref.get(document.body)
document.write(cur[__DOMref_lookupindex] + '<br />' + (
    cur[__DOMref_otherelement].has(document.currentScript)
    ? // if(cur[__DOMref_otherelement].has(document.currentScript)){
    "true"
    : // } else {
    "false"
  )   // }
);

𝗧𝗵𝗲 𝗗𝗶𝗳𝗳𝗲𝗿𝗲𝗻𝗰𝗲

La diferencia puede parecer insignificante, aparte del hecho de que la versión de mapa débil es más larga, sin embargo, hay una gran diferencia entre las dos piezas de código que se muestran arriba. En el primer fragmento de código, sin mapas débiles, el fragmento de código almacena referencias en todos los sentidos entre los elementos DOM. Esto evita que los elementos DOM se recojan basura.(i * i) % lenPuede parecer un bicho raro que nadie usaría, pero piénselo de nuevo: un montón de código de producción tiene referencias DOM que rebotan en todo el documento. Ahora, para el segundo fragmento de código, debido a que todas las referencias a los elementos son débiles, cuando elimina un nodo, el navegador puede determinar que el nodo no se utiliza (su código no puede alcanzarlo), y así eliminarlo de la memoria. La razón por la que debería preocuparse por el uso de la memoria y los anclajes de la memoria (cosas como el primer fragmento de código donde se guardan los elementos no utilizados en la memoria) es porque más uso de la memoria significa más intentos de GC del navegador (para intentar liberar memoria para evitar un bloqueo del navegador) significa una experiencia de navegación más lenta y, a veces, un bloqueo del navegador.

En cuanto a un polyfill para estos, recomendaría mi propia biblioteca (que se encuentra aquí @ github ). Es una biblioteca muy ligera que simplemente lo rellenará sin ninguno de los marcos demasiado complejos que puede encontrar en otros rellenos polivinílicos.

~ ¡Feliz codificación!

Jack Giffin
fuente
1
Gracias por la explicación clara. Un ejemplo vale más que cualquier palabra.
newguy
@lolzery, Re " Esto evita que los elementos DOM se recojan basura ", todo lo que necesita es establecerlo elementsen nulo y listo: se aplicará GC. & Re "Las referencias DOM que rebotan en todo el documento ", no importan en absoluto: una vez que el enlace principal elementsdesaparezca, toda la referencia circular será GC. Si su elemento retiene referencias a elementos que no necesita, entonces arregle el código y configure la referencia como nula cuando haya terminado de usarlo. Será GCed. No se necesitan mapas débiles .
Pacerier
2
@Pacerier, gracias por sus comentarios entusiastas, sin embargo, establecerlo elementsen nulo no permitirá que el navegador GC los elementos en la primera situación de fragmento. Esto se debe a que establece propiedades personalizadas en los elementos, y luego esos elementos aún se pueden obtener, y sus propiedades personalizadas aún se pueden acceder, evitando así que cualquiera de ellos sea GC'ed. Piense en ello como una cadena de anillos de metal. Si tiene acceso a al menos un eslabón de la cadena, puede mantener ese eslabón en la cadena y evitar así que toda la cadena de elementos caiga al abismo.
Jack Giffin
1
código de producción con dunder llamado vars me hace vomitar
Barbu Barbu
10

Lo uso WeakMappara el caché de la memorización sin preocupaciones de funciones que toman objetos inmutables como su parámetro.

La memorización es una forma elegante de decir "después de calcular el valor, almacénelo en caché para que no tenga que volver a calcularlo".

Aquí hay un ejemplo:

Algunas cosas a tener en cuenta:

  • Los objetos Immutable.js devuelven nuevos objetos (con un nuevo puntero) cuando los modifica, por lo que usarlos como claves en un WeakMap garantiza el mismo valor calculado.
  • El WeakMap es excelente para las notas porque una vez que el objeto (utilizado como clave) se recolecta basura, también lo hace el valor calculado en el WeakMap.
Rico Kahler
fuente
1
Este es un uso válido de mapas débiles siempre que la memoria caché de memoria esté destinada a ser sensible a la memoria , no persistente durante la vida útil de obj / function. Si el "caché de la memoria" está destinado a ser persistente durante la vida útil de obj / function, entonces el mapa débil es la elección incorrecta: utilice en su lugar cualquiera de las 6 técnicas de encapsulación de JavaScript predeterminadas .
Pacerier
3

Tengo este caso / ejemplo de uso basado en características simples para WeakMaps.

GESTIONAR UNA COLECCIÓN DE USUARIOS

Me comenzó con un Userobjeto cuyas propiedades incluir una fullname, username, age, gendery un método llamado printque imprime un resumen legible por humanos de las otras propiedades.

/**
Basic User Object with common properties.
*/
function User(username, fullname, age, gender) {
    this.username = username;
    this.fullname = fullname;
    this.age = age;
    this.gender = gender;
    this.print = () => console.log(`${this.fullname} is a ${age} year old ${gender}`);
}

Luego agregué un Mapa llamado userspara mantener una colección de múltiples usuarios que están codificados por username.

/**
Collection of Users, keyed by username.
*/
var users = new Map();

La adición de la Colección también requería funciones auxiliares para agregar, obtener, eliminar un Usuario e incluso una función para imprimir a todos los usuarios en aras de la integridad.

/**
Creates an User Object and adds it to the users Collection.
*/
var addUser = (username, fullname, age, gender) => {
    let an_user = new User(username, fullname, age, gender);
    users.set(username, an_user);
}

/**
Returns an User Object associated with the given username in the Collection.
*/
var getUser = (username) => {
    return users.get(username);
}

/**
Deletes an User Object associated with the given username in the Collection.
*/
var deleteUser = (username) => {
    users.delete(username);
}

/**
Prints summary of all the User Objects in the Collection.
*/
var printUsers = () => {
    users.forEach((user) => {
        user.print();
    });
}

Con todo el código anterior ejecutándose, digamos NodeJS , solo el usersMapa tiene la referencia a los Objetos de usuario en todo el proceso. No hay otra referencia a los Objetos de usuario individuales.

Ejecutando este código un shell interactivo de NodeJS, como ejemplo, agrego cuatro usuarios e los imprimo: Agregar e imprimir usuarios

AGREGAR MÁS INFORMACIÓN A LOS USUARIOS SIN MODIFICAR EL CÓDIGO EXISTENTE

Ahora supongamos que se requiere una nueva función en la que los enlaces de la Plataforma de redes sociales (SMP) de cada usuario deben rastrearse junto con los Objetos de usuario.

La clave aquí también es que esta característica debe implementarse con una intervención mínima al código existente.

Esto es posible con WeakMaps de la siguiente manera.

Agrego tres WeakMaps separados para Twitter, Facebook, LinkedIn.

/*
WeakMaps for Social Media Platforms (SMPs).
Could be replaced by a single Map which can grow
dynamically based on different SMP names . . . anyway...
*/
var sm_platform_twitter = new WeakMap();
var sm_platform_facebook = new WeakMap();
var sm_platform_linkedin = new WeakMap();

Se getSMPWeakMapagrega una función auxiliar simplemente para devolver el WeakMap asociado con el nombre SMP dado.

/**
Returns the WeakMap for the given SMP.
*/
var getSMPWeakMap = (sm_platform) => {
    if(sm_platform == "Twitter") {
        return sm_platform_twitter;
    }
    else if(sm_platform == "Facebook") {
        return sm_platform_facebook;
    }
    else if(sm_platform == "LinkedIn") {
        return sm_platform_linkedin;
    }
    return undefined;
}

Una función para agregar un enlace SMP de usuarios al WeakMap SMP dado.

/**
Adds a SMP link associated with a given User. The User must be already added to the Collection.
*/
var addUserSocialMediaLink = (username, sm_platform, sm_link) => {
    let user = getUser(username);
    let sm_platform_weakmap = getSMPWeakMap(sm_platform);
    if(user && sm_platform_weakmap) {
        sm_platform_weakmap.set(user, sm_link);
    }
}

Una función para imprimir solo los usuarios que están presentes en el SMP dado.

/**
Prints the User's fullname and corresponding SMP link of only those Users which are on the given SMP.
*/
var printSMPUsers = (sm_platform) => {
    let sm_platform_weakmap = getSMPWeakMap(sm_platform);
    console.log(`Users of ${sm_platform}:`)
    users.forEach((user)=>{
        if(sm_platform_weakmap.has(user)) {
            console.log(`\t${user.fullname} : ${sm_platform_weakmap.get(user)}`)
        }
    });
}

Ahora puede agregar enlaces SMP para los usuarios, también con la posibilidad de que cada usuario tenga un enlace en múltiples SMP.

... continuando con el ejemplo anterior, agrego enlaces SMP a los usuarios, enlaces múltiples para los usuarios Bill y Sarah y luego imprimo los enlaces para cada SMP por separado: Agregar enlaces SMP a los usuarios y mostrarlos

Ahora digamos que un usuario se elimina del usersmapa llamando deleteUser. Eso elimina la única referencia al objeto de usuario. Esto a su vez también borrará el enlace SMP de cualquiera / todos los WeakMaps SMP (por Garbage Collection) ya que sin el objeto de usuario no hay forma de acceder a ninguno de sus enlaces SMP.

... continuando con el Ejemplo, elimino al usuario Bill y luego imprimo los enlaces de los SMP con los que estaba asociado:

Al eliminar al usuario Bill del Mapa, también se eliminan los enlaces SMP

No se requiere ningún código adicional para eliminar individualmente el enlace SMP por separado y el código existente antes de esta función no se modificó de ninguna manera.

Si hay alguna otra forma de agregar esta función con / sin WeakMaps, no dude en comentar.

electrocrat
fuente
_____nice______
Aleks