Map e Set são duas estruturas de dados poderosas introduzidas no ES6 (ECMAScript 2015) que ampliam as capacidades de manipulação de coleções em JavaScript. Enquanto objetos e arrays sempre foram as estruturas principais, Map e Set oferecem vantagens específicas para cenários como chaves de qualquer tipo, garantia de unicidade e melhor performance em certas operações. Nesta aula, exploraremos em profundidade cada uma delas, com exemplos práticos, e também veremos as versões fracas (WeakMap e WeakSet), que são úteis para gerenciamento de memória.

Quando usar Map vs objeto

Objetos em JavaScript são tradicionalmente usados como mapas, associando chaves a valores. No entanto, a partir do ES6, o Map foi introduzido como uma alternativa mais especializada. A principal diferença é que em um objeto as chaves são sempre strings (ou símbolos), enquanto em um Map as chaves podem ser qualquer tipo de dado, incluindo objetos, funções e até mesmo outros mapas. Isso torna o Map mais flexível para cenários onde você precisa de chaves não-string.

Além disso, Map oferece métodos dedicados como set, get, has, delete, clear e a propriedade size, que são mais intuitivos e performáticos que a manipulação de objetos. Por exemplo, verificar se uma chave existe em um objeto requer hasOwnProperty ou o operador in, enquanto em Map é simplesmente map.has(chave). Outra vantagem é que Map mantém a ordem de inserção das chaves, o que é garantido pela especificação, enquanto objetos têm ordem apenas para chaves string (não para símbolos) e podem ser imprevisíveis em alguns casos.

Exemplo de uso:

// Objeto tradicional
const obj = {};
obj['nome'] = 'Alice';
obj[1] = 'número';
console.log(obj); // { '1': 'número', nome: 'Alice' } - chave numérica convertida para string

// Map
const map = new Map();
map.set('nome', 'Alice');
map.set(1, 'número');
map.set({ id: 1 }, 'objeto como chave');
console.log(map.get(1)); // 'número'
console.log(map.size); // 3

Quando usar Map? Prefira Map quando: você precisa de chaves de tipos diferentes de string; a quantidade de pares é dinâmica e você fará muitas inserções/remoções; você precisa iterar sobre as chaves/valores facilmente (Map é iterável); ou quando a ordem de inserção é importante. Objetos ainda são melhores para armazenar dados estruturados (como um registro com campos fixos) e quando você precisa de herança de protótipo ou métodos integrados.

Set e unicidade

Set é uma coleção de valores únicos, ou seja, cada valor pode aparecer apenas uma vez. Diferente de arrays, que podem conter duplicatas, Set garante automaticamente que não haja valores repetidos. Isso é extremamente útil para operações como remover duplicatas de um array, verificar se um elemento já foi processado, ou manter uma lista de itens únicos.

Set também oferece métodos como add, has, delete, clear e a propriedade size. A verificação de existência com has é muito mais rápida do que em arrays (complexidade O(1) vs O(n)). Além disso, Set é iterável, permitindo o uso de for...of e métodos como forEach.

Exemplo de remoção de duplicatas:

const array = [1, 2, 2, 3, 4, 4, 5];
const unique = [...new Set(array)];
console.log(unique); // [1, 2, 3, 4, 5]

Outro exemplo útil é verificar se um valor já foi visto:

const visitados = new Set();
function processar(valor) {
  if (visitados.has(valor)) {
    console.log('Já processado');
    return;
  }
  visitados.add(valor);
  console.log('Processando', valor);
}
processar(10); // Processando 10
processar(10); // Já processado

É importante notar que a unicidade é baseada na comparação de igualdade de valor (SameValueZero), que é similar ao operador ===, mas considera NaN igual a NaN (diferente de === que considera NaN !== NaN). Objetos são comparados por referência, então dois objetos diferentes são considerados distintos mesmo que tenham o mesmo conteúdo.

WeakMap/WeakSet (introdução)

WeakMap e WeakSet são versões "fracas" de Map e Set. A principal diferença é que eles não impedem que seus objetos-chave sejam coletados pelo garbage collector se não houver outras referências a eles. Isso os torna ideais para associações temporárias, como metadados ou caches, sem vazar memória.

Em um WeakMap, as chaves devem ser objetos (não podem ser primitivos). Se a única referência a um objeto for como chave no WeakMap, esse objeto pode ser removido da memória, e a entrada correspondente no WeakMap é automaticamente removida. O WeakMap não possui métodos de iteração (keys, values, entries) nem a propriedade size, pois o conteúdo pode mudar a qualquer momento devido à coleta de lixo.

Exemplo de uso com WeakMap para armazenar dados privados:

const privados = new WeakMap();

class Pessoa {
  constructor(nome) {
    privados.set(this, { nome });
  }
  getNome() {
    return privados.get(this).nome;
  }
}

const p = new Pessoa('João');
console.log(p.getNome()); // 'João'
console.log(privados.get(p)); // { nome: 'João' }
// Quando p for destruído, a entrada em privados será removida automaticamente

WeakSet funciona de forma análoga: só aceita objetos e não impede a coleta de lixo. É útil para marcar objetos como "visitados" sem interferir na vida útil deles. Por exemplo, em algoritmos de grafos, você pode usar um WeakSet para rastrear nós já processados, sem impedir que os nós sejam coletados se não forem mais usados.

Exemplo com WeakSet:

const processados = new WeakSet();

function processar(objeto) {
  if (processados.has(objeto)) return;
  processados.add(objeto);
  // ... processamento
}

O uso de WeakMap e WeakSet é avançado e geralmente recomendado para bibliotecas ou frameworks que precisam gerenciar memória de forma eficiente. Para a maioria dos casos do dia a dia, Map e Set são suficientes.

Boas práticas

Ao trabalhar com Map e Set, lembre-se de que eles são iteráveis, então você pode usar for...of e o spread operator. Prefira map.size em vez de Object.keys(obj).length para performance. Evite usar objetos como mapas quando as chaves são dinâmicas ou de tipos variados. Para conjuntos de valores únicos, Set é mais adequado que arrays com verificações manuais de duplicatas. Quanto a WeakMap/WeakSet, use-os apenas quando precisar de associações fracas para evitar vazamentos de memória.

Referências

Exercícios

  1. Crie um Map que associe nomes de países a suas capitais. Adicione pelo menos 5 países e, em seguida, verifique se "Brasil" está presente e exiba sua capital.

    ✓ Resposta:
    const capitais = new Map([
      ['Brasil', 'Brasília'],
      ['França', 'Paris'],
      ['Japão', 'Tóquio'],
      ['Alemanha', 'Berlim'],
      ['Canadá', 'Ottawa']
    ]);
    
    if (capitais.has('Brasil')) {
      console.log(capitais.get('Brasil')); // 'Brasília'
    }
    
  2. Dado um array de números com duplicatas, use Set para retornar um novo array sem duplicatas.

    ✓ Resposta:
    const numeros = [1, 2, 2, 3, 4, 4, 5, 5, 6];
    const unicos = [...new Set(numeros)];
    console.log(unicos); // [1, 2, 3, 4, 5, 6]
    
  3. Escreva uma função que receba um array de strings e retorne um Map onde cada chave é uma string e o valor é o número de vezes que ela aparece no array.

    ✓ Resposta:
    function contarFrequencia(arr) {
      const freq = new Map();
      for (const item of arr) {
        freq.set(item, (freq.get(item) || 0) + 1);
      }
      return freq;
    }
    
    const palavras = ['maçã', 'banana', 'maçã', 'laranja', 'banana', 'maçã'];
    console.log(contarFrequencia(palavras));
    // Map { 'maçã' => 3, 'banana' => 2, 'laranja' => 1 }
    
  4. Explique a diferença entre WeakMap e Map. Dê um exemplo de situação onde WeakMap é mais adequado.

    ✓ Resposta:

    WeakMap aceita apenas objetos como chave e não impede a coleta de lixo desses objetos. Se a única referência a um objeto for como chave no WeakMap, ele pode ser removido da memória. Map mantém referências fortes, impedindo a coleta. WeakMap não é iterável e não possui size. Um exemplo de uso adequado é armazenar dados privados associados a instâncias de objetos, como em classes, para evitar vazamento de memória quando a instância é destruída.

  5. Crie um WeakSet e adicione dois objetos. Verifique se um dos objetos está presente no WeakSet.

    ✓ Resposta:
    const ws = new WeakSet();
    const obj1 = { id: 1 };
    const obj2 = { id: 2 };
    ws.add(obj1);
    ws.add(obj2);
    
    console.log(ws.has(obj1)); // true
    console.log(ws.has(obj2)); // true
    
    // Se obj1 for sobrescrito, ele pode ser coletado
    obj1 = null;
    // Neste ponto, o WeakSet pode remover automaticamente obj1