Map e Set: coleções além de arrays e objetos

1. Por que usar Map e Set? Limitações de objetos e arrays

1.1. Objetos comuns: chaves apenas string/Symbol, iteração limitada e herança de protótipo

Objetos JavaScript tradicionais possuem limitações significativas quando usados como dicionários ou mapas. As chaves são automaticamente convertidas para strings (ou Symbols), o que pode causar comportamentos inesperados:

const objeto = {};
objeto[1] = 'número';
objeto['1'] = 'string';
console.log(objeto); // { '1': 'string' } — a chave numérica sobrescreveu a string

// Herança de protótipo pode causar falsos positivos
const dados = {};
console.log(dados.toString); // [Function: toString] — herda do protótipo
console.log('toString' in dados); // true — mesmo sem ter definido

1.2. Arrays como conjuntos: busca ineficiente (O(n)) e duplicatas indesejadas

Arrays são ótimos para listas ordenadas, mas péssimos como conjuntos:

const ids = [1, 2, 3, 1, 4, 2];
// Verificar existência é O(n)
console.log(ids.includes(3)); // true — percorre todo o array

// Remover duplicatas exige código extra
const semDuplicatas = [...new Set(ids)]; // [1, 2, 3, 4]

1.3. Casos reais: cache, contagem de frequência, dados associativos complexos

// Cache com objeto — frágil com chaves não-string
const cache = {};
const usuario1 = { id: 1 };
cache[usuario1] = { dados: '...' }; // chave vira "[object Object]"

// Map resolve isso naturalmente
const cacheMap = new Map();
cacheMap.set(usuario1, { dados: '...' });

2. Map: o dicionário moderno

2.1. Sintaxe básica

const mapa = new Map();

mapa.set('nome', 'João');
mapa.set(42, 'resposta');
mapa.set({ id: 1 }, 'objeto como chave');
mapa.set(NaN, 'Not a Number');

console.log(mapa.get('nome')); // 'João'
console.log(mapa.has(42));     // true
console.log(mapa.size);        // 4

mapa.delete('nome');
console.log(mapa.size);        // 3

2.2. Chaves de qualquer tipo

Diferente de objetos, Map aceita qualquer valor como chave, mantendo a identidade do objeto:

const mapa = new Map();
const funcao = () => {};
const obj = {};

mapa.set(funcao, 'função como chave');
mapa.set(obj, 'objeto como chave');
mapa.set(document, 'DOM element');

console.log(mapa.get(funcao)); // 'função como chave'
console.log(mapa.get(obj));    // 'objeto como chave'

2.3. Iteração nativa

const usuarios = new Map([
  [1, 'Alice'],
  [2, 'Bob'],
  [3, 'Charlie']
]);

// forEach
usuarios.forEach((valor, chave) => {
  console.log(`${chave}: ${valor}`);
});

// for...of com entries(), keys(), values()
for (const [id, nome] of usuarios) {
  console.log(`ID ${id}: ${nome}`);
}

console.log([...usuarios.keys()]);   // [1, 2, 3]
console.log([...usuarios.values()]); // ['Alice', 'Bob', 'Charlie']

3. Set: conjuntos sem duplicatas

3.1. Criação e manipulação

const conjunto = new Set();

conjunto.add(1);
conjunto.add(2);
conjunto.add(1); // ignorado — já existe
conjunto.add('texto');

console.log(conjunto.has(1));    // true
console.log(conjunto.has(3));    // false
console.log(conjunto.size);      // 3

conjunto.delete(2);
console.log(conjunto.size);      // 2

3.2. Remoção automática de duplicatas

const comDuplicatas = [1, 2, 2, 3, 3, 3, 4];
const semDuplicatas = [...new Set(comDuplicatas)];
console.log(semDuplicatas); // [1, 2, 3, 4]

// Útil para IDs únicos de API
const respostas = await Promise.all([
  fetch('/api/users/1'),
  fetch('/api/users/2'),
  fetch('/api/users/1') // requisição duplicada
]);
const idsUnicos = [...new Set(respostas.map(r => r.url))];

3.3. Operações de conjuntos

const setA = new Set([1, 2, 3, 4]);
const setB = new Set([3, 4, 5, 6]);

// União
const uniao = new Set([...setA, ...setB]);
console.log([...uniao]); // [1, 2, 3, 4, 5, 6]

// Interseção
const intersecao = new Set([...setA].filter(x => setB.has(x)));
console.log([...intersecao]); // [3, 4]

// Diferença (A - B)
const diferenca = new Set([...setA].filter(x => !setB.has(x)));
console.log([...diferenca]); // [1, 2]

4. Performance e gerenciamento de memória

4.1. Comparação de desempenho

// Map vs Objeto para inserção/busca frequente
const tamanho = 100000;

// Objeto
const obj = {};
console.time('objeto-insercao');
for (let i = 0; i < tamanho; i++) obj[`key${i}`] = i;
console.timeEnd('objeto-insercao');

// Map
const map = new Map();
console.time('map-insercao');
for (let i = 0; i < tamanho; i++) map.set(`key${i}`, i);
console.timeEnd('map-insercao');
// Map geralmente é mais rápido para inserção/deleção frequente

4.2. WeakMap e WeakSet

WeakMap e WeakSet permitem que objetos sejam coletados pelo garbage collector quando não há mais referências:

const weakMap = new WeakMap();
let usuario = { id: 1 };

weakMap.set(usuario, { metadados: 'sensível' });
console.log(weakMap.has(usuario)); // true

usuario = null; // o objeto pode ser coletado
// weakMap.get(usuario) agora retorna undefined

4.3. Garbage collection com WeakMap em listeners

// Em Node.js ou React — evitar memory leaks
const listeners = new WeakMap();

function adicionarListener(elemento, callback) {
  if (!listeners.has(elemento)) {
    listeners.set(elemento, new Set());
  }
  listeners.get(elemento).add(callback);
  elemento.addEventListener('click', callback);
}

// Quando elemento for removido do DOM e perder referências,
// o WeakMap permite que tudo seja coletado automaticamente

5. Map e Set no ecossistema Node.js

5.1. Cache de requisições HTTP com Map

const express = require('express');
const app = express();

const cache = new Map();

app.get('/api/users/:id', async (req, res) => {
  const { id } = req.params;

  if (cache.has(id)) {
    console.log('Cache hit');
    return res.json(cache.get(id));
  }

  const dados = await fetchUserFromDB(id);
  cache.set(id, dados);

  // Expiração simples após 5 minutos
  setTimeout(() => cache.delete(id), 5 * 60 * 1000);

  res.json(dados);
});

5.2. Controle de sessões e usuários únicos

const sessoesAtivas = new Set();

function login(usuarioId) {
  if (sessoesAtivas.has(usuarioId)) {
    throw new Error('Usuário já está logado');
  }
  sessoesAtivas.add(usuarioId);
}

function logout(usuarioId) {
  sessoesAtivas.delete(usuarioId);
}

console.log(sessoesAtivas.size); // 0
login('user123');
console.log(sessoesAtivas.size); // 1

5.3. Serialização com JSON

const mapa = new Map([
  ['nome', 'João'],
  ['idade', 30],
  ['cidade', 'São Paulo']
]);

// Map não serializa diretamente com JSON.stringify
const serializado = JSON.stringify([...mapa]);
console.log(serializado);
// [["nome","João"],["idade",30],["cidade","São Paulo"]]

// Desserialização
const restaurado = new Map(JSON.parse(serializado));
console.log(restaurado.get('nome')); // 'João'

6. React: estado e dados reativos com Map e Set

6.1. Estado complexo com Map

import React, { useState } from 'react';

function ListaItens() {
  const [itens, setItens] = useState(new Map());

  const adicionarItem = (id, nome) => {
    setItens(prev => new Map(prev).set(id, { nome, completo: false }));
  };

  const toggleItem = (id) => {
    setItens(prev => {
      const novo = new Map(prev);
      const item = novo.get(id);
      novo.set(id, { ...item, completo: !item.completo });
      return novo;
    });
  };

  return (
    <ul>
      {[...itens.entries()].map(([id, item]) => (
        <li key={id} onClick={() => toggleItem(id)}>
          {item.nome} {item.completo ? '✓' : ''}
        </li>
      ))}
    </ul>
  );
}

6.2. Seleção múltipla com Set

function SelecaoCheckboxes({ opcoes }) {
  const [selecionados, setSelecionados] = useState(new Set());

  const toggle = (id) => {
    setSelecionados(prev => {
      const novo = new Set(prev);
      if (novo.has(id)) {
        novo.delete(id);
      } else {
        novo.add(id);
      }
      return novo;
    });
  };

  return (
    <div>
      {opcoes.map(opcao => (
        <label key={opcao.id}>
          <input
            type="checkbox"
            checked={selecionados.has(opcao.id)}
            onChange={() => toggle(opcao.id)}
          />
          {opcao.nome}
        </label>
      ))}
      <p>Selecionados: {selecionados.size}</p>
    </div>
  );
}

6.3. Performance com cache

function ComponentePesado({ dados }) {
  const [cache, setCache] = useState(new Map());

  const processarDado = useCallback((id) => {
    if (cache.has(id)) {
      return cache.get(id);
    }

    const resultado = computacaoPesada(dados.find(d => d.id === id));
    setCache(prev => new Map(prev).set(id, resultado));
    return resultado;
  }, [dados, cache]);

  return (
    <div>
      {dados.map(dado => (
        <Item key={dado.id} resultado={processarDado(dado.id)} />
      ))}
    </div>
  );
}

7. Padrões avançados e boas práticas

7.1. Map como dicionário polimórfico

const metadados = new Map();

function adicionarMetadados(objeto, chave, valor) {
  if (!metadados.has(objeto)) {
    metadados.set(objeto, new Map());
  }
  metadados.get(objeto).set(chave, valor);
}

const usuario = { nome: 'João' };
adicionarMetadados(usuario, 'criadoEm', Date.now());
adicionarMetadados(usuario, 'ultimoAcesso', Date.now());

7.2. Set para rastreamento de promises pendentes

const promisesPendentes = new Set();

async function requisicaoControlada(url) {
  if (promisesPendentes.has(url)) {
    throw new Error('Requisição já em andamento');
  }

  const promise = fetch(url);
  promisesPendentes.add(url);

  try {
    return await promise;
  } finally {
    promisesPendentes.delete(url);
  }
}

7.3. Map de Sets para relacionamentos muitos-para-muitos

const alunosPorCurso = new Map();

function matricularAluno(cursoId, alunoId) {
  if (!alunosPorCurso.has(cursoId)) {
    alunosPorCurso.set(cursoId, new Set());
  }
  alunosPorCurso.get(cursoId).add(alunoId);
}

function alunosDoCurso(cursoId) {
  return alunosPorCurso.get(cursoId) || new Set();
}

matricularAluno('react-101', 'aluno1');
matricularAluno('react-101', 'aluno2');
matricularAluno('node-201', 'aluno1');

console.log([...alunosDoCurso('react-101')]); // ['aluno1', 'aluno2']

Referências