JavaScript Map
O que é Map?
Map é uma estrutura de dados key-value (chave-valor) nativa do JavaScript que permite armazenar pares de dados de qualquer tipo como chave ou valor.
const map = new Map();
map.set('nome', 'João');
map.set(1, 'número um');
map.set(true, 'booleano');
map.set({id: 1}, 'objeto como chave');
Estrutura de Dados Interna
Map é implementada como uma Hash Table (Tabela Hash):
┌─────────────────────────────────────────────────────────────┐
│ HASH TABLE │
├─────────────────────────────────────────────────────────────┤
│ Bucket 0: [key1, value1] -> [key4, value4] │
│ Bucket 1: [key2, value2] │
│ Bucket 2: [key3, value3] -> [key5, value5] │
│ Bucket 3: empty │
│ ... │
└─────────────────────────────────────────────────────────────┘
Como funciona internamente:
-
Hash Function: Converte a chave em um índice do array, ou seja, digamos que a chave e
banana
, ele converte essa chave em um numero especifico, usando algorimitos de hasheamento avancados. -
Collision Handling: Usa chaining (encadeamento) para resolver colisões, isso significa que, as vezes quando uma chave e convertida em um indice, pode ser que esse indice conflite com um outro ja existente, oque o Map faz e em cada indice ao invez de ter apenas um valor, se tem um array, podendo armazenar mais de um valor por indice, a funcao
.get
do Map lida em encontrar dentro desse array qual o item certo, mas colisoes sao raras pois o algoritimo de Hash e bem avancado. - Dynamic Resizing: Redimensiona automaticamente quando necessário.
Big O
Operação | Complexidade | Explicação |
---|---|---|
set(key, value) | O(1) amortizado | Hash direto para o bucket |
get(key) | O(1) amortizado | Hash direto para busca |
has(key) | O(1) amortizado | Hash direto para verificação |
delete(key) | O(1) amortizado | Hash direto para remoção |
size | O(1) | Propriedade mantida internamente |
clear() | O(n) | Precisa limpar todos os elementos |
forEach() | O(n) | Itera sobre todos os elementos |
O que significa „amortizado“?
Amortizado é um termo técnico usado quando a complexidade de uma operação pode na verdade ter duas complexidades distintas, dependendo de dois cenários diferentes que podem ocorrer.
O que acontece no Map
é que o tamanho in-memory
(na memória) de um Map
quando ele é iniciado é sempre 4, ou seja, 4 slots de memória são reservados para o Map. Nos 3 primeiros elementos que são adicionados ao Map, a operação tem complexidade O(1), mas no momento que o quarto elemento for adicionado, será necessário criar um novo espaço in-memory
de tamanho 8, e mover todos os elementos atuais para o novo espaço, em uma função que se assemelharia a essa:
// Processo interno do redimensionamento:
function resize() {
const oldTable = this.table; // O(1)
this.table = new Array(oldTable.length * 2); // O(1)
this.size = 0; // O(1)
// AQUI está o O(n)!
for (const bucket of oldTable) { // O(n)
for (const [key, value] of bucket) {
this.set(key, value); // Rehash cada elemento
}
}
}
Apenas neste caso específico, a operação executada será O(n), mas como estes casos só vão acontecer cada vez que o Map
tiver que duplicar seu tamanho, é dito que a complexidade foi „amortizada“ para um caso mais realista.
A sequência de redimensionamentos funciona assim:
- Inicialmente será a inserção do elemento número 4 que será O(n) (com n=4)
- Depois esse número vai aumentar exponencialmente: a segunda vez será a inserção do elemento número 8 que será O(n) (com n=8)
- Depois a inserção do elemento 16, depois a 32, depois a 64, depois a 128, e assim por diante
Chegando em um ponto que a operação O(n) ficará tão distante uma da outra que pode ser desconsiderada em casos reais de uso. Por exemplo, se você tem um Map com 1 milhão de elementos, o próximo redimensionamento só acontecerá quando chegar a 2 milhões de elementos – uma distância muito grande entre as operações custosas.
Por isso a complexidade é chamada de „amortizada“: na média, considerando uma sequência longa de operações, o custo das operações caras (redimensionamento) é distribuído entre as operações baratas (inserções normais), resultando em uma complexidade média de O(1) por operação.
Map vs Object vs Array
Aspecto | Map | Object | Array |
---|---|---|---|
Tipos de chave | Qualquer tipo | String/Symbol | Números (índices) |
Tamanho | map.size |
Object.keys(obj).length |
arr.length |
Iteração | Preserva ordem de inserção | Não garantida (ES5) | Preserva ordem |
Acesso | map.get(key) |
obj.key ou obj[key]
|
arr[index] |
Performance | O(1) para tudo | O(1) para acesso | O(1) para acesso |
Exemplos Práticos
1. Criação e Operações Básicas
const map = new Map();
// Adicionar
map.set('nome', 'Maria');
map.set(1, 'primeiro');
map.set(true, 'verdadeiro');
// Buscar
console.log(map.get('nome')); // "Maria"
console.log(map.get(1)); // "primeiro"
// Verificar existência
console.log(map.has('nome')); // true
console.log(map.has('age')); // false
// Tamanho
console.log(map.size); // 3
// Remover
map.delete('nome');
console.log(map.size); // 2
2. Chaves de Qualquer Tipo
const map = new Map();
// Objetos como chaves
const obj1 = {id: 1};
const obj2 = {id: 2};
map.set(obj1, 'primeiro objeto');
map.set(obj2, 'segundo objeto');
console.log(map.get(obj1)); // "primeiro objeto"
console.log(map.get({id: 1})); // undefined (diferente objeto!)
3. Iteração
const map = new Map([
['a', 1],
['b', 2],
['c', 3]
]);
// Iterar chaves
for (const key of map.keys()) {
console.log(key); // 'a', 'b', 'c'
}
// Iterar valores
for (const value of map.values()) {
console.log(value); // 1, 2, 3
}
// Iterar pares
for (const [key, value] of map.entries()) {
console.log(key, value); // 'a' 1, 'b' 2, 'c' 3
}
// forEach
map.forEach((value, key) => {
console.log(key, value);
});
4. Inicialização com Array
const map = new Map([
['key1', 'value1'],
['key2', 'value2'],
['key3', 'value3']
]);
// Converter Map para Array
const array = Array.from(map);
console.log(array); // [['key1', 'value1'], ['key2', 'value2'], ['key3', 'value3']]
Quando Usar Map?
✅ Use Map quando:
- Precisa de chaves que não são strings
- Precisa saber o tamanho frequentemente
- Precisa iterar em ordem de inserção
- Precisa de performance O(1) garantida
- Chaves são dinâmicas/desconhecidas
❌ Use Object quando:
- Chaves são sempre strings
- Precisa de sintaxe mais simples
- Precisa de serialização JSON
- Tem métodos/propriedades específicas
Exemplo Prático: Contagem de Caracteres
function countChars(str) {
const map = new Map();
for (const char of str) {
if (map.has(char)) {
map.set(char, map.get(char) + 1);
} else {
map.set(char, 1);
}
}
return map;
}
const result = countChars("hello world");
console.log(result);
// Map { 'h' => 1, 'e' => 1, 'l' => 3, 'o' => 2, ' ' => 1, 'w' => 1, 'r' => 1, 'd' => 1 }
Métodos Úteis
const map = new Map();
// Operações básicas
map.set(key, value); // Adiciona/atualiza
map.get(key); // Obtém valor
map.has(key); // Verifica existência
map.delete(key); // Remove
map.clear(); // Remove todos
map.size; // Tamanho
// Iteração
map.keys(); // Iterator das chaves
map.values(); // Iterator dos valores
map.entries(); // Iterator dos pares [key, value]
map.forEach(callback); // Itera com callback
Map é a estrutura perfeita para casos que precisam de mapeamento chave-valor com performance O(1) e flexibilidade de tipos!