Como sabemos no capítulo Coleta de lixo, o mecanismo JavaScript mantém um valor na memória enquanto ele está “acessível” e pode ser potencialmente usado.
Por exemplo:
deixe john = {nome: "John" }; // o objeto pode ser acessado, john é a referência para ele //sobrescreve a referência João = nulo; //o objeto será removido da memória
Normalmente, as propriedades de um objeto ou elementos de uma matriz ou outra estrutura de dados são consideradas alcançáveis e mantidas na memória enquanto essa estrutura de dados está na memória.
Por exemplo, se colocarmos um objeto em um array, enquanto o array estiver ativo, o objeto também estará vivo, mesmo que não haja outras referências a ele.
Assim:
deixe john = {nome: "John" }; deixe matriz = [joão]; João = nulo; //sobrescreve a referência // o objeto referenciado anteriormente por john é armazenado dentro do array // portanto não será coletado como lixo // podemos obtê-lo como array[0]
Semelhante a isso, se usarmos um objeto como chave em um Map
regular, enquanto o Map
existir, esse objeto também existirá. Ele ocupa memória e não pode ser coletado como lixo.
Por exemplo:
deixe john = {nome: "John" }; deixe mapa = novo Mapa(); map.set(joão, "..."); João = nulo; //sobrescreve a referência // john é armazenado dentro do mapa, // podemos obtê-lo usando map.keys()
WeakMap
é fundamentalmente diferente neste aspecto. Não impede a coleta de lixo de objetos-chave.
Vamos ver o que isso significa com exemplos.
A primeira diferença entre Map
e WeakMap
é que as chaves devem ser objetos, não valores primitivos:
deixe fracoMap = new WeakMap(); deixe obj = {}; fracoMap.set(obj, "ok"); //funciona bem (chave do objeto) //não é possível usar uma string como chave fracoMap.set("teste", "Opa"); // Erro, porque "test" não é um objeto
Agora, se usarmos um objeto como chave nele, e não houver outras referências a esse objeto – ele será removido da memória (e do mapa) automaticamente.
deixe john = {nome: "John" }; deixe fracoMap = new WeakMap(); fracoMap.set(joão, "..."); João = nulo; //sobrescreve a referência // john é removido da memória!
Compare-o com o exemplo normal Map
acima. Agora, se john
existir apenas como chave do WeakMap
– ele será automaticamente excluído do mapa (e da memória).
WeakMap
não suporta iteração e métodos keys()
, values()
, entries()
, portanto não há como obter todas as chaves ou valores dele.
WeakMap
possui apenas os seguintes métodos:
weakMap.set(key, value)
weakMap.get(key)
weakMap.delete(key)
weakMap.has(key)
Por que tal limitação? Isso é por razões técnicas. Se um objeto tiver perdido todas as outras referências (como john
no código acima), ele deverá ser coletado como lixo automaticamente. Mas tecnicamente não é especificado exatamente quando a limpeza acontece .
O mecanismo JavaScript decide isso. Ele pode optar por realizar a limpeza da memória imediatamente ou esperar e fazer a limpeza mais tarde, quando ocorrerem mais exclusões. Portanto, tecnicamente, a contagem atual de elementos de um WeakMap
não é conhecida. O motor pode ter limpado ou não, ou parcialmente. Por esse motivo, os métodos que acessam todas as chaves/valores não são suportados.
Agora, onde precisamos dessa estrutura de dados?
A principal área de aplicação do WeakMap
é o armazenamento adicional de dados .
Se estamos trabalhando com um objeto que “pertence” a outro código, talvez até uma biblioteca de terceiros, e gostaríamos de armazenar alguns dados associados a ele, que só deveriam existir enquanto o objeto estiver ativo – então WeakMap
é exatamente o que está acontecendo. necessário.
Colocamos os dados em um WeakMap
, usando o objeto como chave, e quando o objeto for coletado como lixo, esses dados também desaparecerão automaticamente.
fracoMap.set(john, "documentos secretos"); // se John morrer, documentos secretos serão destruídos automaticamente
Vejamos um exemplo.
Por exemplo, temos um código que mantém uma contagem de visitas dos usuários. A informação é armazenada em um mapa: um objeto de usuário é a chave e a contagem de visitas é o valor. Quando um usuário sai (seu objeto é coletado como lixo), não queremos mais armazenar sua contagem de visitas.
Aqui está um exemplo de função de contagem com Map
:
// ? visitasCount.js deixe visitasCountMap = new Map(); // mapa: usuário => contagem de visitas //aumenta a contagem de visitas function contagemUsuário(usuário) { deixe contar =visitasCountMap.get(user) || 0; visitasCountMap.set(usuário, contagem + 1); }
E aqui está outra parte do código, talvez outro arquivo que o utilize:
// ? principal.js deixe john = {nome: "John" }; contaUsuário(joão); //conta as visitas dele // mais tarde John nos deixa João = nulo;
Agora, o objeto john
deve ser coletado como lixo, mas permanece na memória, pois é uma chave em visitsCountMap
.
Precisamos limpar visitsCountMap
quando removemos usuários, caso contrário ele crescerá na memória indefinidamente. Essa limpeza pode se tornar uma tarefa tediosa em arquiteturas complexas.
Podemos evitá-lo mudando para WeakMap
:
// ? visitasCount.js deixe visitasCountMap = new WeakMap(); // mapa fraco: usuário => contagem de visitas //aumenta a contagem de visitas function contagemUsuário(usuário) { deixe contar =visitasCountMap.get(user) || 0; visitasCountMap.set(usuário, contagem + 1); }
Agora não precisamos limpar visitsCountMap
. Depois que o objeto john
se torna inacessível, exceto como uma chave de WeakMap
, ele é removido da memória, junto com as informações dessa chave de WeakMap
.
Outro exemplo comum é o cache. Podemos armazenar (“cache”) os resultados de uma função, para que futuras chamadas no mesmo objeto possam reutilizá-lo.
Para conseguir isso, podemos usar Map
(cenário não ideal):
// ? cache.js deixe cache = novo Mapa(); //calcula e lembra o resultado função processo(obj) { se (!cache.has(obj)) { let result = /* cálculos do resultado para */ obj; cache.set(obj,resultado); resultado de retorno; } retornar cache.get(obj); } // Agora usamos process() em outro arquivo: // ? principal.js let obj = {/* digamos que temos um objeto */}; deixe resultado1 = processo (obj); // calculado // ...mais tarde, de outro local do código... deixe resultado2 = processo (obj); // resultado lembrado retirado do cache // ...mais tarde, quando o objeto não for mais necessário: obj = nulo; alerta(cache.size); // 1 (Ai! O objeto ainda está no cache, ocupando memória!)
Para múltiplas chamadas de process(obj)
com o mesmo objeto, ele apenas calcula o resultado na primeira vez e depois retira-o do cache
. A desvantagem é que precisamos limpar cache
quando o objeto não for mais necessário.
Se substituirmos Map
por WeakMap
, esse problema desaparecerá. O resultado armazenado em cache será removido da memória automaticamente depois que o objeto for coletado como lixo.
// ? cache.js deixe cache = new WeakMap(); //calcula e lembra o resultado função processo(obj) { se (!cache.has(obj)) { deixe resultado = /* calcule o resultado para */ obj; cache.set(obj,resultado); resultado de retorno; } retornar cache.get(obj); } // ? principal.js deixe obj = {/* algum objeto */}; deixe resultado1 = processo (obj); deixe resultado2 = processo (obj); // ...mais tarde, quando o objeto não for mais necessário: obj = nulo; // Não é possível obter cache.size, pois é um WeakMap, // mas é 0 ou logo será 0 // Quando obj é coletado como lixo, os dados em cache também serão removidos
WeakSet
se comporta de forma semelhante:
É análogo a Set
, mas só podemos adicionar objetos ao WeakSet
(não primitivos).
Um objeto existe no conjunto enquanto pode ser acessado de outro lugar.
Assim como Set
, ele suporta add
, has
e delete
, mas não size
, keys()
e nenhuma iteração.
Por ser “fraco”, também serve como armazenamento adicional. Mas não para dados arbitrários, mas sim para factos “sim/não”. Uma associação ao WeakSet
pode significar algo sobre o objeto.
Por exemplo, podemos adicionar usuários ao WeakSet
para acompanhar quem visitou nosso site:
deixe visitadoSet = new WeakSet(); deixe john = {nome: "John" }; deixe pete = {nome: "Pete" }; deixe Maria = { nome: "Maria" }; visitadoSet.add(joão); // John nos visitou visitadoSet.add(pete); //Então Pete visitadoSet.add(joão); //João novamente //visitSet tem 2 usuários agora // verifica se John visitou? alerta(visitedSet.has(joão)); // verdadeiro // verifica se Mary visitou? alerta(visitedSet.has(mary)); // falso João = nulo; //visitSet será limpo automaticamente
A limitação mais notável do WeakMap
e WeakSet
é a ausência de iterações e a incapacidade de obter todo o conteúdo atual. Isso pode parecer inconveniente, mas não impede que WeakMap/WeakSet
faça seu trabalho principal – ser um armazenamento “adicional” de dados para objetos que são armazenados/gerenciados em outro local.
WeakMap
é uma coleção semelhante a Map
que permite apenas objetos como chaves e os remove junto com o valor associado quando se tornam inacessíveis por outros meios.
WeakSet
é uma coleção semelhante a Set
que armazena apenas objetos e os remove quando se tornam inacessíveis por outros meios.
Suas principais vantagens são que possuem fraca referência a objetos, podendo ser facilmente removidos pelo coletor de lixo.
Isso tem o custo de não ter suporte para clear
, size
, keys
, values
…
WeakMap
e WeakSet
são usados como estruturas de dados “secundárias”, além do armazenamento de objetos “primário”. Depois que o objeto for removido do armazenamento primário, se ele for encontrado apenas como a chave do WeakMap
ou em um WeakSet
, ele será limpo automaticamente.
importância: 5
Há uma série de mensagens:
deixe mensagens = [ {texto: "Olá", de: "John"}, {texto: "Como vai?", de: "John"}, {texto: "Até breve", de: "Alice"} ];
Seu código pode acessá-lo, mas as mensagens são gerenciadas pelo código de outra pessoa. Novas mensagens são adicionadas, as antigas são removidas regularmente por esse código e você não sabe os momentos exatos em que isso acontece.
Agora, qual estrutura de dados você poderia usar para armazenar informações sobre se a mensagem “foi lida”? A estrutura deve ser adequada para dar a resposta “foi lido?” para o objeto de mensagem fornecido.
PS Quando uma mensagem é removida do messages
, ela também deve desaparecer da sua estrutura.
PPS Não devemos modificar objetos de mensagens, adicionar nossas propriedades a eles. Como eles são gerenciados pelo código de outra pessoa, isso pode levar a consequências ruins.
Vamos armazenar mensagens lidas em WeakSet
:
deixe mensagens = [ {texto: "Olá", de: "John"}, {texto: "Como vai?", de: "John"}, {texto: "Até breve", de: "Alice"} ]; deixe readMessages = new WeakSet(); //duas mensagens foram lidas readMessages.add(mensagens[0]); readMessages.add(mensagens[1]); // readMessages tem 2 elementos // ...vamos ler a primeira mensagem novamente! readMessages.add(mensagens[0]); // readMessages ainda possui 2 elementos únicos // resposta: a mensagem[0] foi lida? alert("Ler mensagem 0: " + readMessages.has(messages[0])); // verdadeiro mensagens.shift(); // agora readMessages tem 1 elemento (tecnicamente a memória pode ser limpa mais tarde)
O WeakSet
permite armazenar um conjunto de mensagens e verificar facilmente a existência de uma mensagem nele.
Ele se limpa automaticamente. A desvantagem é que não podemos iterar sobre isso, não podemos obter “todas as mensagens lidas” diretamente dele. Mas podemos fazer isso iterando todas as mensagens e filtrando aquelas que estão no conjunto.
Outra solução diferente poderia ser adicionar uma propriedade como message.isRead=true
a uma mensagem depois de lida. Como os objetos de mensagens são gerenciados por outro código, isso geralmente não é recomendado, mas podemos usar uma propriedade simbólica para evitar conflitos.
Assim:
// a propriedade simbólica só é conhecida pelo nosso código deixe isRead = Símbolo("isRead"); mensagens[0][isRead] = true;
Agora, o código de terceiros provavelmente não verá nossa propriedade extra.
Embora os símbolos permitam diminuir a probabilidade de problemas, usar WeakSet
é melhor do ponto de vista arquitetônico.
importância: 5
Há uma série de mensagens como na tarefa anterior. A situação é semelhante.
deixe mensagens = [ {texto: "Olá", de: "John"}, {texto: "Como vai?", de: "John"}, {texto: "Até breve", de: "Alice"} ];
A questão agora é: qual estrutura de dados você sugeriria para armazenar a informação: “quando a mensagem foi lida?”.
Na tarefa anterior precisávamos apenas armazenar o fato “sim/não”. Agora precisamos armazenar a data, e ela só deve permanecer na memória até que a mensagem seja coletada como lixo.
As datas PS podem ser armazenadas como objetos da classe Date
integrada, que abordaremos mais tarde.
Para armazenar uma data, podemos usar WeakMap
:
deixe mensagens = [ {texto: "Olá", de: "John"}, {texto: "Como vai?", de: "John"}, {texto: "Até breve", de: "Alice"} ]; deixe readMap = new WeakMap(); readMap.set(mensagens[0], nova data(2017, 1, 1)); // Objeto data que estudaremos mais tarde