JavaScript: Map vs Object, quando e como usar?
Tags: ptbr, javascript, webdev, node
Published: 2019-07-10 12:55:15

Você pode se perguntar - por que Map vs Object, e não Map vs Array, ou Objeto vs Set? Bem, você também pode comparar entre qualquer um dos dois, mas o Map e Object, ao contrário dos outros, tem casos de uso muito semelhantes, que nos obrigam a entender mais profundamente cada um deles para decidir o que é melhor e para quando. E é disso que esse artigo trata.

Devemos começar, vamos?

# Primeiros Conceitos

## O que é Map?

Map parece muito simples, não é? Nós vemos ou ouvimos sobre isso quase todos os dias, digamos, Mapa do Mundo, Mapa de Ruas, etc... então, o que é exatamente um Map? 😆

Map é um tipo de coleção de dados (de uma forma mais sofisticada -  um tipo de estrutura de dados abstrata), em que os dados são **armazenados em pares**, que contêm uma **chave única** e um **valor mapeado para essa chave**. E devido à essa singularidade de cada chave armazenada, **não há par duplicado** na coleção.

Você pode reconhecer agora uma coisa comum sobre todos os exemplos mencionados acima - eles são usados ​​para procurar algo (pode ser um país - no caso do Mapa do Mundo, um nome de rua - Mapa de Ruas, etc…).

É isso mesmo, o Map é usado principalmente para **pesquisar e procurar dados rapidamente**.

Por exemplo, `{(1, "smile"), (2, "cry"), (42, "happy")}` 

Em que cada par está no formato: `(chave, valor)`.

Nota importante: **a chave e o valor em Map podem estar em qualquer tipo de dados**, não limitados a apenas *string* ou *numbers*.

## E quanto ao Object?

Todo mundo conhece o Object, especialmente em JavaScript! Object é object, não é mesmo? Certo, mas isso não é o suficiente. 😅

**Object Regular** (preste atenção à palavra "regular") em JavaScript é um tipo de coleção de dados no formato dicionário - o que significa que também segue um **conceito de chave-valor para armazenar os dados**, como o Map. Cada **chave** em Object - ou normalmente a chamamos de "propriedade" - também é **única** e está **associada a um único valor**.

Além disso, o **Object** em JavaScript tem um **protótipo interno**. E não se esqueça, quase todos os objetos em JavaScript são instâncias de Object, incluindo Map.

Por exemplo, `{1: "smile", 2: "cry", 42: "happy"}` 

Portanto, por definição, **Object** e **Map** são baseados no mesmo conceito - usando chave-valor para armazenar dados. No entanto, como sempre dizemos -  *iguais, mas diferentes*  - eles são de fato muito diferentes um do outro, principalmente em:

- **O campo da chave:** em **Object**, segue a regra do dicionário normal. As chaves devem ser tipos simples  - seja *number* ou *string* ou *symbols*. Nada mais. Mas no mapa pode ser qualquer tipo de dados (um objeto, um array, etc. Tente usar outro objeto como chave de propriedade do objeto - eu te desafio! :))
- **Ordem dos elementos**: no **Map**, a **ordem original dos elementos (pares) é preservada**, enquanto que em **Object não é**.
- **Herança:** **Map** é uma instância de Object (surpresa!). Mas **Object** definitivamente **não é** uma instância de Map.

```js
var map = new Map([[1,2],[3,4]]);
console.log(map instanceof Object); // true

var obj = new Object();
console.log(obj instanceof Map); // false
```

Mas não é só isso. O que mais os torna diferentes um do outro? Vamos continuar.

## Inicialização

## Object

Como o Array, Object é direto. Para declarar novo objeto, tudo que você precisa fazer é usar o literal direto:

```js
var obj = {}; // Objeto vazio 
var obj = {id: 1, nome: "Objeto de teste"}; 
// 2 chaves aqui: `id` mapeia para 1 e `nome` para "Objeto de teste"
```

Ou pelo construtor:

```js
var obj = new Object(); // Objeto vazio
var obj = new Object; // Mesmo resultado
```

Ou usando **Object.prototype.create**:

```js
var obj = Object.create(null); // Objeto vazio
```

Nota especial:

Você só deve usar **Object.create** em casos muito específicos, como:

- Você deseja escolher o objeto para herdar o protótipo, sem a necessidade de definir o construtor. É uma maneira de "herança".

```js
var Vehicle = {
    type: "General",
    display: function(){console.log(this.type);}
}
var Car = Object.create(Vehicle); // cria um novo `Car` herdando do `Vehicle`

Car.type = "Car"; // sobrescreve a propriedade
Car.display(); // "Car"

Vehicle.display(); // ainda "Geral"
```

Em geral, como em **Array**, **não use** o construtor interno ao invés do valor literal ao criar um novo objeto, porque:

- Mais digitação
- Desempenho mais lento (muito mais lento)
- Confusão e aumentando mais chances de erro, por exemplo:

```js
var obj = new Object(id: 1, name: "test") // Obviamente um erro

var obj1 = {id: 1, name: "test"};
var obj2 = new Object(obj1); // obj1 e obj2 apontam para o mesmo objeto

obj2.id = 2;
console.log(obj1.id); //2
```

Em todo caso, quem quer digitar código extra desnecessariamente? 😁

## Map

O **Map**, por outro lado, tem apenas uma maneira de inicialização, usando seu construtor embutido e **nova** sintaxe.

```js
var map = new Map(); // Mapa vazio
var map = new Map([[1,2],[2,3]]); // map = {1=>2, 2=>3}
```

> Map([iterável])

O construtor recebe um **array** ou **objeto iterável** cujos elementos são pares de chave-valor - também conhecidos como arrays com 2 elementos `[chave, valor]`. 

Por enquanto, tudo bem? Ótimo. Agora é hora de avançar para nossa próxima etapa: comparação entre os recursos básicos do **Map/Object**, que são:

## Acessando Elementos

- No **Map**, acessar um valor de elemento é feito por **Map.prototype.get(chave)** - o que significa que precisamos saber a chave para poder recuperar o valor de um elemento

```js
map.get (1) // 2
```

- Já em **Object**, precisamos conhecer a chave/propriedade para obter o valor do elemento, e temos diferentes sintaxes: **Object.<chave>** e **Object['chave']**:

```js
obj.id // 1
obj['id'] // 1
```

- Verificar se uma chave já existe no **Map** é suportado usando:

```js
map.has(1);// retorna um valor boolean:  true/false
```

- Enquanto em **Object**, precisamos fazer um pouco mais:

```js
var isExist = obj.id === undefined; // verifica se obj tem aquela propriedade definida.
```

- Ou:

```js
var isExist = 'id' in obj; // que também se aplica à propriedades herdadas.
```

A sintaxe no **Map** é mais simples e mais direta do que no **Object**, neste caso.

Nota: em **Object**, temos **Object.prototype.hasOwnProperty()** retornando **true/false** para verificar se ele tem a chave especificada como sua própria propriedade - isso será bom no caso de verificarmos apenas uma chave que não é herdada para aquele objeto. Ainda assim, na minha opinião, aqui o Map ganha sobre o Object quanto à facilidade de uso.

## Adicionando novo elemento

- O Map suporta a adição de novo elemento, fornecendo **Map.prototype.set()**, que recebe 2 parâmetros: chave, valor.

```js
map.set(4,5); // {1 => 2, 2 => 3, 4 => 5}
```

- Mas se você passar uma chave existente, ela sobrescreverá o valor mapeado para aquela chave com o novo valor - como a operação **set** deve fazer.

```js
map.set(4,6); // {1 => 2, 2 => 3, 4 => 6}
```

- Da mesma forma, adicionar um novo conjunto de propriedades ao **Object** é feito diretamente por:

```js
obj['gender'] = 'female';
// {id: 1, nome: "test", gender: "female"} 

obj.gender = male; 
// Ambos são OK e sobrescreverão o valor mapeado existente se a propriedade já existir.

// {id: 1, name: "test", gender: "male"}
```

- Como você pode ver, ambos executam teoricamente o tempo de execução em **O(1)** para adicionar elementos graças à sua estrutura, portanto, a recuperação de uma chave não requer a varredura de todos os dados. 

E quanto a remover/excluir um elemento?

## Removendo/Excluindo um Elemento

Em **Object**, não há nenhum método interno para excluir uma propriedade dele. Em vez disso, podemos usar o operador **delete**:

```js
delete obj.id; // {name: "test", gender: "male"}
```

Preste atenção que algumas pessoas podem dizer para fazer o seguinte, para aumentar o desempenho:

```js
obj.id = undefined;
```

No entanto, a lógica é bem diferente aqui:

- **delete(chave)** irá **remover essa propriedade específica completamente** do objeto
- mas definindo **‌obj[chave] = undefined** na verdade **apenas alterou o valor mapeado dessa propriedade para "undefined"**, e essa propriedade ainda permanece em seu lugar nesse objeto.

Portanto, quando usamos **"for..in"** , ainda iremos fazer iterações sobre a chave dessa propriedade, independentemente do seu valor.

E, claro, a verificação para saber se uma chave/propriedade já existe em um **Object** produzirá dois resultados diferentes nesses dois cenários, exceto a seguinte verificação:

```js
obj.id === undefined; // mesmo resultado
```

Portanto, pense com cuidado. Aumento de desempenho, por vezes, não vale a pena! :)

Ah mais uma coisa, o operador **delete** retorna uma *string* de **"true/false"**, mas diferentemente do normal, esse valor retornado indica um status bastante diferente, em que:

- **true** para todos os casos, **exceto** quando a propriedade é uma **propriedade não configurável**.
- caso contrário, **false** para o modo não estrito e o erro de exceção será lançado no modo estrito ("use strict").

Enquanto isso, o **Map**, novamente, possui métodos integrados para suportar diferentes finalidades de remoção, como:

- **.delete(chave)** para remover um elemento alvo com uma chave especificada em um Map. Não se esqueça, esse **.delete()** retorna um valor boolean, indicando se o elemento de destino da chave especificada existia no Map e foi removida com sucesso (true) ou se esse elemento alvo não existe no Map (false).

```js
var isDeleteSucceeded = map.delete(1); // { 2=>3, 4=>5}
console.log(isDeleteSucceeded); // true
```

- **.clear()** remove **todos os elementos** de um objeto Map.

```js
map.clear (); // {}
```

- Para obter o mesmo recurso de **.clear()** em **Object**, você precisará iterar por meio de suas propriedades (chaves) e excluir uma por uma até o final. Isso pode ser cansativo, especialmente quando nos sentimos um pouco preguiçosos (só um pouquinho :))

Em geral, o desempenho de Map e Object na remoção de elemento é bastante semelhante entre ambos, novamente devido à sua estrutura. A exclusão de uma chave levará **O(1)**, enquanto a limpeza de todos os elementos ainda levará **O(n)** com **n** sendo o tamanho de Map/Object. Então, sim, é definitivamente um empate aqui!

Já que mencionamos um pouco sobre o tamanho, vamos ver como o **Map/Object** se comporta:

## Obtendo o tamanho

Uma vantagem aqui, que Map tem em comparação com Object, é que o Map mantém seu tamanho atualizado automaticamente, e sempre podemos obter seu tamanho facilmente por:

```js
console.log(map.size); // 0
```

Enquanto com **Object**, precisamos calcular manualmente, com a ajuda de **Object.keys()** - que retorna um array de todas as chaves existentes em um determinado objeto:

```js
console.log(Object.keys(obj).length); // 2
```

Ainda por aqui? Ótimo. Agora, a última comparação, uma vez que marca uma das diferenças significativas entre Map e Object - iteração entre os elementos.

## Iterando

**Map** é iterável por padrão, **Objeto** não é. Simples assim.

- **Bônus:** como você verifica se um tipo é iterável? Usando:

```js
// typeof <obj>[Symbol.iterator] === “function”

console.log(typeof obj[Symbol.iterator]); // undefined
console.log(typeof map[Symbol.iterator]); // function
```

O que significa que no **Map** todos os elementos podem ser iterados diretamente com **"for..in"** como:

```js
// para o Map: { 2=>3, 4=>5}
for (const item of map){
    console.log(item); 
    // Array[2,3]
    // Array[4,5]
}

// ou
for (const [key,value] of map){
    console.log(`key: ${key}, value: ${value}`);
    // key: 2, value: 3
    // key: 4, value: 5
}
```

Ou com seu método interno **.forEach()**:

```js
map.forEach((value, key) => 
  console.log(`key: ${key}, value: ${value}`)
);
// key: 2, value: 3
// key: 4, value: 5
```

Mas com **Object**, nós usamos **"for..in"**:

```js
{id: 1, name: "test"}
for (var key in obj){
   console.log(`key: ${key}, value: ${obj[key]}`);
   // key: id, value: 1
   // key: name, value: test
}
```

Ou usando **Object.keys(obj)** para obter todas as chaves e iterar:

```js
Object.keys(obj).forEach((key)=> 
  console.log(`key: ${key}, value: ${obj[key]}`)
);
// key: id, value: 1
// key: name, value: test
```

OK, aí vem a pergunta - já que eles são realmente similares entre si tanto na estrutura quanto no desempenho, com o Map tendo um pouco mais de vantagens em relação ao Object, devemos sempre preferir Map ao invés de Object?

![](https://cdn-images-1.medium.com/max/1600/1*S5qLQOPOTPQBTDQoUZpt0Q.gif)

# Quando usar Map? E quando usar Object?

Novamente, apesar de todas as vantagens que o Map pode ter contra o Object, ainda há casos em que o Object funciona melhor. Afinal, Object é o conceito mais básico de Javascript.

- **Object** é a melhor opção para cenários em que precisamos apenas de estrutura simples para armazenar dados e sabemos que todas as chaves são **strings** ou **numbers** (ou **symbols**), porque criar objetos simples e acessar a propriedade do objeto com uma chave específica é muito mais rápido do que criar um Map (literal vs construtor, acesso direto vs chamada de função get() - você sabe quem já ganha!).
- Além disso, em cenários onde há a necessidade de aplicar lógica separada à propriedade/elementos individuais, então **Object** é definitivamente a escolha. Por exemplo:

```js
var obj = {
    id: 1, 
    name: "It's Me!", 
    print: function(){ 
        return `Object Id: ${this.id}, with Name: ${this.name}`;
    }
}
console.log(obj.print());// Object Id: 1, with Name: It's Me.
```

*(Tente fazer o mesmo com Map. Você simplesmente não pode!)*

- Além disso, JSON tem suporte direto para o Object, mas não com o Map (ainda). Portanto, em certas situações em que temos que trabalhar muito com JSON, considere Object como a opção preferida.
- Caso contrário, o Map é puramente uma tabela hash, o Object é mais do que isso (com suporte para lógica interna). E usar o operador **delete** com propriedades de Object tem vários problemas de desempenho (discutiremos sobre isso em um artigo diferente). Portanto, em cenários que exigem muita adição e remoção (especialmente) de novos pares, o Map pode ter um desempenho muito melhor.
- Além disso, o Map preserva a ordem de suas chaves - diferente de Object, e Map foi criado com iteração em mente, portanto, caso a iteração ou a ordem de elementos sejam altamente significativas, considere Map  -  ele garantirá desempenho de iteração estável em todos os navegadores.
- E por último, mas não menos importante, o Map tende a ter um melhor desempenho no armazenamento de um grande conjunto de dados, especialmente quando as chaves são desconhecidas até o tempo de execução e quando todas as chaves são do mesmo tipo e todos os valores são do mesmo tipo.

---

# Concluindo

Isso realmente depende do tipo de dados (de entrada) com os quais você irá trabalhar e de quais operações você executará neles, a fim de preferir um ao outro entre Map e Object.

O Map tende a ter mais vantagens sobre o Object em cenários em que precisamos apenas de uma estrutura de pesquisa simples para armazenamento de dados, com todas as operações básicas fornecidas. No entanto, Map não pode nunca substituir Object, em qualquer sentido, porque em JavaScript, Object é - afinal -  mais do que apenas uma tabela hash normal (e portanto não deve ser usada como uma tabela hash normal se houver alternativa, é apenas um desperdício de um grande recurso;)).

Agora, honestamente, qual você gosta mais? :) Me conte nos comentários. Eu adoraria saber sua opinião.

# Créditos 

- [ES6 — Map vs Object — What and when?](https://medium.com/front-end-weekly/es6-map-vs-object-what-and-when-b80621932373), escrito originalmente por [@mayashavin](https://twitter.com/MayaShavin)