JavaScript: Map vs Object, quando e como usar?

Image for post
Image for post
Map ou Object? Qual devo usar?

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 é 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?

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.
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

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:

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

Ou usando Object.prototype.create:

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”.
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:
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

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

map.get (1) // 2
  • Já em Object, precisamos conhecer a chave/propriedade para obter o valor do elemento, e temos diferentes sintaxes: Object. e Object[‘chave’]:
obj.id // 1
obj['id'] // 1
  • Verificar se uma chave já existe no Map é suportado usando:
map.has(1);// retorna um valor boolean:  true/false
  • Enquanto em Object, precisamos fazer um pouco mais:
var isExist = obj.id === undefined; // verifica se obj tem aquela propriedade definida.
  • Ou:
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

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.
map.set(4,6); // {1 => 2, 2 => 3, 4 => 6}
  • Da mesma forma, adicionar um novo conjunto de propriedades ao Object é feito diretamente por:
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

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

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

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:

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).
var isDeleteSucceeded = map.delete(1); // { 2=>3, 4=>5}
console.log(isDeleteSucceeded); // true
  • .clear() remove todos os elementos de um objeto Map.
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

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:

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

  • Bônus: como você verifica se um tipo é iterável? Usando:
// 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:

// 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():

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”:

{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:

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?

Image for post

Quando usar Map? E quando usar Object?

  • 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:
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

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

Written by

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store