Quais são os usos reais do ES6 WeakMap?

397

Quais são os usos reais da WeakMapestrutura de dados introduzidos no ECMAScript 6?

Como uma chave de um mapa fraco cria uma forte referência ao seu valor correspondente, garantindo que um valor que foi inserido em um mapa fraco nunca desapareça enquanto sua chave ainda estiver ativa, ela não poderá ser usada para tabelas de notas, caches ou qualquer outra coisa para a qual você normalmente usaria referências fracas, mapas com valores fracos etc. para.

Parece-me que isto:

weakmap.set(key, value);

... é apenas uma maneira indireta de dizer isso:

key.value = value;

Que casos de uso concretos estão faltando?

valderman
fonte
35
Caso de uso no mundo real: armazene dados customizados para nós DOM.
Felix Kling #
Todos os casos de uso mencionados para referências fracas também são super importantes. Eles são muito mais difíceis de adicionar à linguagem, pois introduzem o não-determinismo. Mark Miller e outros têm trabalhado muito em referências fracas e acho que eles estão chegando. Eventualmente
Benjamin Gruenbaum
2
WeakMaps pode ser usado para detectar vazamentos de memória: stevehanov.ca/blog/?id=148
theWebalyst

Respostas:

513

Fundamentalmente

O WeakMaps fornece uma maneira de estender objetos de fora sem interferir na coleta de lixo. Sempre que você deseja estender um objeto, mas não pode porque está selado - ou de uma fonte externa - um WeakMap pode ser aplicado.

Um WeakMap é um mapa (dicionário) em que as chaves são fracas - ou seja, se todas as referências à chave forem perdidas e não houver mais referências ao valor - o valor poderá ser coletado como lixo. Vamos mostrar isso primeiro por meio de exemplos, depois explicar um pouco e finalmente terminar com o uso real.

Digamos que estou usando uma API que me fornece um determinado objeto:

var obj = getObjectFromLibrary();

Agora, eu tenho um método que usa o objeto:

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

Quero acompanhar quantas vezes o método foi chamado com um determinado objeto e relatar se isso ocorrer mais de N vezes. Ingenuamente, alguém poderia pensar em usar um 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);
}

Isso funciona, mas há um vazamento de memória - agora controlamos todos os objetos da biblioteca passados ​​para a função que impede que os objetos da biblioteca sejam coletados como lixo. Em vez disso, podemos usar um 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);
}

E o vazamento de memória se foi.

Casos de uso

Alguns casos de uso que causariam um vazamento de memória e são ativados por WeakMaps incluem:

  • Manter dados privados sobre um objeto específico e dar acesso apenas a pessoas com referência ao Mapa. Uma abordagem mais ad-hoc está chegando com a proposta de símbolos particulares, mas é muito tempo a partir de agora.
  • Manter dados sobre objetos da biblioteca sem alterá-los ou incorrer em despesas gerais.
  • Manter dados sobre um pequeno conjunto de objetos em que existem muitos objetos do tipo para não incorrer em problemas com classes ocultas que os mecanismos JS usam para objetos do mesmo tipo.
  • Manter dados sobre objetos host, como nós DOM no navegador.
  • Adicionando um recurso a um objeto de fora (como o exemplo do emissor de evento na outra resposta).

Vamos olhar para um uso real

Pode ser usado para estender um objeto de fora. Vamos dar um exemplo prático (adaptado, meio real - para fazer um ponto) do mundo real do Node.js.

Digamos que você seja Node.js e tenha Promiseobjetos - agora você deseja acompanhar todas as promessas atualmente rejeitadas - no entanto, não deseja impedir que elas sejam coletadas como lixo, caso não exista referência a elas.

Agora, você não deseja adicionar propriedades a objetos nativos por razões óbvias - para ficar preso. Se você mantiver referências às promessas, está causando um vazamento de memória, pois nenhuma coleta de lixo pode acontecer. Se você não mantiver referências, não poderá salvar informações adicionais sobre promessas individuais. Qualquer esquema que envolva salvar o ID de uma promessa significa inerentemente que você precisa de uma referência a ela.

Digite WeakMaps

WeakMaps significa que as teclas estão fracas. Não há maneiras de enumerar um mapa fraco ou obter todos os seus valores. Em um mapa fraco, você pode armazenar os dados com base em uma chave e, quando a chave é coletada como lixo, os valores também.

Isso significa que, dada uma promessa, você pode armazenar um estado sobre ele - e esse objeto ainda pode ser coletado como lixo. Posteriormente, se você receber uma referência a um objeto, poderá verificar se possui algum estado relacionado a ele e relatá-lo.

Isso foi usado para implementar ganchos de rejeição sem tratamento 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
});

Mantemos informações sobre promessas em um mapa e podemos saber quando uma promessa rejeitada foi tratada.

Benjamin Gruenbaum
fonte
8
Olá! Você poderia me dizer qual parte do código de exemplo causa vazamento de memória?
Ltamajs 28/10/16
15
@ ltamajs4 claro, no useObjexemplo usando a Mape não a WeakMap, usamos o objeto passado como chave do mapa. O objeto nunca é removido do mapa (já que não saberíamos quando fazer isso), portanto sempre há uma referência a ele e nunca pode ser coletado como lixo. No exemplo WeakMap, assim que todas as outras referências ao objeto desaparecerem - o objeto pode ser limpo do WeakMap. Se você ainda não tem certeza o que quero dizer por favor me avise
Benjamin Gruenbaum
@ Benjamin, precisamos distinguir entre a necessidade de um cache sensível à memória e a necessidade de uma tupla data_object. Não confunda esses dois requisitos separados. Seu calledexemplo é melhor escrito usando jsfiddle.net/f2efbm7z e não demonstra o uso de um mapa fraco. De fato, pode ser melhor escrito de um total de 6 maneiras, que listarei abaixo.
Pacerier 18/09/17
Fundamentalmente, o objetivo do mapa fraco é um cache sensível à memória. Embora possa ser usado para estender objetos de fora, esse é um truque desnecessário e definitivamente não é o seu objetivo adequado .
Pacerier 18/09/17
11
Se você deseja manter o vínculo entre uma promessa e o número de vezes que ela foi tratada / rejeitada, use 1) símbolo; p[key_symbol] = data. ou 2) nomeação única; p.__key = data. ou 3) escopo privado; (()=>{let data; p.Key = _=>data=_;})(). ou 4) proxy com 1 ou 2 ou 3. ou 5) substituir / estender a classe Promise por 1 ou 2 ou 3. ou 6) substituir / estender a classe Promise por uma tupla de membros necessários. - Em qualquer caso, um mapa fraco não é necessário, a menos que você precise de um cache sensível à memória.
Pacerier 18/09/19
48

Essa resposta parece ser tendenciosa e inutilizável em um cenário do mundo real. Leia como está e não a considere como uma opção real para outra coisa que não a experimentação

Um caso de uso pode ser usá-lo como um dicionário para ouvintes, tenho um colega de trabalho que fez isso. É muito útil porque qualquer ouvinte é direcionado diretamente para essa maneira de fazer as coisas. Adeuslistener.on .

Mas, de um ponto de vista mais abstrato, WeakMapé especialmente poderoso para desmaterializar o acesso a basicamente qualquer coisa, você não precisa de um espaço para nome para isolar seus membros, pois ele já está implícito na natureza dessa estrutura. Tenho certeza de que você poderia fazer algumas melhorias importantes na memória, substituindo chaves de objeto redundantes estranhas (mesmo que a desconstrução faça o trabalho para você).


Antes de ler o que vem a seguir

Agora percebo que minha ênfase não é exatamente a melhor maneira de resolver o problema e, como Benjamin Gruenbaum apontou (confira sua resposta, se já não estiver acima da minha: p), esse problema não poderia ter sido resolvido regularmente Map, pois teria vazado, portanto, o principal ponto forte WeakMapé que ele não interfere na coleta de lixo, uma vez que eles não mantêm uma referência.


Aqui está o código real do meu colega de trabalho (graças a ele para compartilhamento)

Fonte completa aqui , é sobre gerenciamento de ouvintes de que falei acima (você também pode dar uma olhada nas especificações )

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
fonte
2
Ainda não entendo como você usaria isso. Isso faria com que o observável fosse recolhido junto com os eventos vinculados a ele quando não fossem mais referenciados. O problema que tendem a ter é quando o Observador não é mais referenciado. Eu acho que a solução aqui resolveu apenas metade do problema. Eu não acho que você possa resolver o problema do observador com o WeakMap, pois ele não é iterável.
Jgmjgm
11
Os ouvintes de eventos com buffer duplo podem ser rápidos em outros idiomas, mas nesse caso é simplesmente esotérico e lento. Esses são meus três centavos.
Jack Giffin
@axelduch, Uau, esse mito do identificador de ouvintes foi espalhado por toda a comunidade Javascript, ganhando 40 votos positivos! Para entender por que essa resposta está completamente errada , consulte os comentários em stackoverflow.com/a/156618/632951
Pacerier
11
O @Pacerier atualizou a resposta, obrigado pelo feedback.
axelduch
11
@axelduch, Sim, há um juiz de lá também.
Pacerier 18/09
18

WeakMap funciona bem para encapsulamento e ocultação de informações

WeakMapestá disponível apenas para ES6 e acima. A WeakMapé uma coleção de pares de chave e valor em que a chave deve ser um objeto. No exemplo a seguir, criamos um WeakMapcom dois itens:

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

Usamos o set()método para definir uma associação entre um objeto e outro item (uma string no nosso caso). Usamos o get()método para recuperar o item associado a um objeto. O aspecto interessante do WeakMaps é o fato de ele conter uma referência fraca à chave dentro do mapa. Uma referência fraca significa que, se o objeto for destruído, o coletor de lixo removerá toda a entrada da WeakMap, liberando memória.

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
fonte
4
Re "weakmap funciona bem para encapsulamento e ocultação de informações". Só porque você pode, não significa que você deveria. O Javascript possui maneiras padrão de encapsulamento e ocultação de informações mesmo antes de o mapa fraco ser inventado. No momento, existem literalmente 6 maneiras de fazer isso . Usar o fracamap para encapsular é uma facepalm feia.
Pacerier 18/09/17
12

𝗠𝗲𝘁𝗮𝗱𝗮𝘁𝗮

Mapas fracos podem ser usados ​​para armazenar metadados sobre elementos DOM sem interferir na coleta de lixo ou deixar os colegas de trabalho zangados com seu código. Por exemplo, você pode usá-los para indexar numéricos todos os elementos em uma página da 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"
  )   // }
);

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

A diferença pode parecer insignificante, além do fato de a versão fraca do mapa ser maior, no entanto, há uma grande diferença entre os dois trechos de código mostrados acima. No primeiro trecho de código, sem mapas fracos, o trecho de código armazena referências de todas as formas entre os elementos DOM. Isso evita que os elementos DOM sejam coletados como lixo.(i * i) % lenpode parecer uma coisa esquisita que ninguém usaria, mas pense novamente: bastante código de produção possui referências DOM que saltam por todo o documento. Agora, para o segundo trecho de código, porque todas as referências aos elementos são fracas, quando você remove um nó, o navegador pode determinar que o nó não é usado (não pode ser alcançado pelo seu código) e exclua-o da memória. A razão pela qual você deve se preocupar com o uso de memória e as âncoras de memória (coisas como o primeiro trecho de código em que elementos não utilizados são mantidos na memória) é porque mais uso de memória significa mais tentativas de GC do navegador (para tentar liberar memória para evitar uma falha no navegador) significa uma experiência de navegação mais lenta e, às vezes, uma falha no navegador.

Quanto a um polyfill para esses, eu recomendaria minha própria biblioteca ( encontrada aqui no github ). É uma biblioteca muito leve que simplesmente o preenche com polyfill sem nenhuma das estruturas complexas demais que você pode encontrar em outros polyfills.

~ Feliz codificação!

Jack Giffin
fonte
11
Obrigado pela explicação clara. Um exemplo vale mais do que qualquer palavra.
newguy
@ lololery, Re " Isso evita que os elementos DOM sejam coletados de lixo ", tudo que você precisa é definir elementscomo nulo e pronto: será GCed. & Re " Referências DOM que saltam por todo o documento ", não importa: elementsQuando o link principal se for, todas as referências circulares serão GCed. Se o seu elemento estiver mantendo referências ao elemento que não precisa, corrija o código e defina o ref como null quando terminar de usá-lo. Será GCed. Mapas fracos não são necessários .
Pacerier 18/09
2
@Pacerier, obrigado por seus comentários entusiasmados, no entanto, a configuração elementspara null não permitirá que o navegador faça o GC dos elementos na primeira situação de snippet. Isso ocorre porque você define propriedades customizadas nos elementos e, em seguida, esses elementos ainda podem ser obtidos e suas propriedades customizadas ainda podem ser acessadas, impedindo que qualquer um deles seja GC'ed. Pense nisso como uma corrente de anéis de metal. Quando você tem acesso a pelo menos um elo da cadeia, pode segurá-lo e impedir que toda a cadeia de itens caia no abismo.
Jack Giffin
11
código de produção com dunder chamado vars me faz vomitar
Barbu Barbu
10

eu uso WeakMap para o cache de memorização sem preocupações de funções que recebem objetos imutáveis ​​como parâmetro.

Memoização é uma maneira elegante de dizer "depois de calcular o valor, armazene-o em cache para que você não precise calculá-lo novamente".

Aqui está um exemplo:

Algumas coisas a serem observadas:

  • Os objetos Immutable.js retornam novos objetos (com um novo ponteiro) quando você os modifica, portanto, usá-los como chaves em um WeakMap garante o mesmo valor calculado.
  • O WeakMap é ótimo para memorandos porque, uma vez que o objeto (usado como chave) é coletado como lixo, o mesmo acontece com o valor calculado no WeakMap.
Rico Kahler
fonte
11
Esse é um uso válido do fracomap, desde que o cache de memorização seja sensível à memória , não persistente durante toda a vida útil da obj / função. Se o "cache de memorização" for persistente durante toda a vida útil da obj / função, omap fraco é a escolha errada: use uma das 6 técnicas de encapsulamento javascript padrão .
Pacerier 18/09
3

Eu tenho esse caso de uso baseado em recursos simples / exemplo para WeakMaps.

GERIR UMA COLEÇÃO DE USUÁRIOS

Comecei com um Userobjeto cujas propriedades incluem um fullname, username, age, gendere um método chamado printque imprime um resumo legível das outras propriedades.

/**
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}`);
}

Em seguida, adicionei um mapa chamado userspara manter uma coleção de vários usuários digitados por username.

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

A adição da coleção também exigia funções auxiliares para adicionar, obter, excluir um usuário e até uma função para imprimir todos os usuários por uma questão de integridade.

/**
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();
    });
}

Com todo o código acima em execução, digamos NodeJS , apenas o usersMapa tem a referência aos Objetos do Usuário em todo o processo. Não há outra referência aos objetos de usuário individuais.

Executando este código um shell NodeJS interativo, apenas como um exemplo, adiciono quatro usuários e os imprimo: Adicionando e imprimindo usuários

ADICIONE MAIS INFORMAÇÃO AOS USUÁRIOS SEM MODIFICAR O CÓDIGO EXISTENTE

Agora, digamos que um novo recurso seja necessário, em que os links de SMP (Social Media Platform) de cada usuário precisam ser rastreados junto com os Objetos de Usuário.

A chave aqui é também que esse recurso deve ser implementado com o mínimo de intervenção no código existente.

Isso é possível com o WeakMaps da seguinte maneira.

Eu adiciono três 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();

Uma função auxiliar getSMPWeakMapé adicionada simplesmente para retornar o WeakMap associado ao nome SMP fornecido.

/**
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;
}

Uma função para adicionar o link SMP de um usuário ao SMP WeakMap fornecido.

/**
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);
    }
}

Uma função para imprimir apenas os usuários presentes no SMP especificado.

/**
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)}`)
        }
    });
}

Agora você pode adicionar links SMP para os usuários, também com a possibilidade de cada usuário ter um link em vários SMPs.

... continuando com o exemplo anterior, adiciono links SMP aos usuários, vários links para os usuários Bill e Sarah e imprimo os links para cada SMP separadamente: Adicionando links SMP aos usuários e exibindo-os

Agora diga que um usuário é excluído do usersmapa chamando deleteUser. Isso remove a única referência ao objeto de usuário. Isso, por sua vez, também limpará o link SMP de qualquer / todos os SMP WeakMaps (por Garbage Collection), pois sem o Objeto do Usuário, não há como acessar qualquer link SMP.

... continuando com o exemplo, excluo o usuário Bill e imprimo os links dos SMPs aos quais ele foi associado:

A exclusão da conta do usuário do mapa também remove os links SMP

Não há necessidade de qualquer código adicional para excluir individualmente o link SMP separadamente e o código existente antes que esse recurso não fosse modificado.

Se houver outra maneira de adicionar esse recurso com / sem WeakMaps, fique à vontade para comentar.

eletrocrata
fonte
_____agradável______
Aleks