JavaScript Maps: Entendendo o conceito

Desde o início da humanidade procuramos relacionar coisas em objetos de chave e valor, por exemplo, uma lista de convidados de uma festa, listas de presença de aulas, contagem de praticamente qualquer coisa e por ai vai.

La Pascaline, a primeira calculadora mecânica do mundo

Quando as primeiras máquinas programáveis surgiram lá atrás com as calculadoras mecânicas, um novo problema foi criado: como podemos transpor listas de chave e valor para uma estrutura computacional que fosse, ao mesmo tempo, facilmente manipulável e segura?

E então surgiram os Hash Maps.

E então fez-se os maps

Se você já está acostumado a trabalhar com Java, provavelmente já deve ter ouvido falar ou até usado um hash map. Eles são as implementações mais comuns de objetos iteráveis contendo chaves e valores ou, como são também chamados, dictionaries (o pessoal de C# pira).

Dictionaries em C#

Em linguagens fortemente tipadas (como o Java e o C#), temos que especificar quais são os tipos tanto da chave quanto do valor que vamos armazenar (veja o <int, string> ), mas como fazemos isso em Javascript? Uma linguagem tão dinâmica que nem os próprio programadores dão conta dela?

Bom, a resposta para isso é bastante simples: não fazemos. Como o Javascript já possui objetos de chave e valor naturalmente (muito como o Python e o PHP) não precisamos fazer nenhum tratamento especial, então isso nos cria uma estrutura de fácil iteração, mas e a segurança?

Um uso bastante comum para os maps do Javascript é quando mapeamos chaves em formato string para um valor arbitrário, como abaixo:

Vamos criar uma lista de cães, obviamente, mas temos um problema meio complicado com esse design de projeto:

  • Segurança: Imagine que uma das chaves se chame __proto__ ou toString ou qualquer coisa dentro de Object.prototype então vamos ter um grande problema, como eu já disse no meu artigo sobre protótipos, nunca é uma boa ideia mexer com eles diretamente. Fora que estamos criando um comportamento praticamente imprevisível no nosso código.
  • As iterações sobre estes itens vão ficar bastante verbosas com Object.keys(canil).forEach , a não ser que você implemente um iterador, o que é bem verboso também.
  • As chaves estão limitadas a simples strings então fica complicado criar outras chaves de outros tipos que não são strings ou que são apenas referências

A primeira solução

Para solucionar o primeiro problema de segurança, basta adicionarmos um prefixo no início da chave, isso faz com que ela seja diferente de qualquer coisa nativa:

Mas, por sorte, o ES6 veio par resolver os problemas que tínhamos no ES5.

Maps no ES6

Com o advento do ES6 temos uma estrutura específica para lidar com maps. Ela se chama, pasmem, Map . Vamos ver como seria se convertêssemos nosso código anterior para esta nova estrutura:

Muito mais simples não?

A principal diferença aqui é que podemos usar qualquer coisa como uma chave. Não estamos mais apenas limitados a tipos primitivos, mas podemos usar também funções, objetos, datas, qualquer coisa.

É óbvio que muitas dessas coisas não fazem sentido prático, mas são modelos possíveis de serem usados.

A outra mudança significativa é que o Map é um iterable e produz uma coleção de valores mais ou menos do tipo [ ['chave', 'valor'], ['chave', 'valor'] ] .

Outra forma de se inicializar um Map

O que colocamos acima é a mesma coisa de usarmos map.set para cada valor que inserirmos. É meio burro adicionarmos os itens um por um quando podemos simplesmente incluir um iterável todo no nosso map direto. O que nos dá o spread operator como um efeito colateral:

E, como temos o poder do iterador na nossa mão, podemos misturar tudo: destructuring, for ... of , template literals e etc:

E, agora que já sabemos um pouco sobre os hash maps, podemos falar de algo que pode não ter ficado claro inicialmente. Apesar de possuir uma implementação de uma API de adição, todas as chaves são únicas, o que significa que, se ficarmos escrevendo em uma chave, ela só irá sobrescrever seu próprio valor:

O caso do NaN

Esse caso é importante de ser destacado.

Acho que todos sabemos que o NaN é um monstro muito estranho do Javascript que se comporta de algumas formas bem… Exóticas…

“Naturalmente”, uma expressão do tipo NaN !== NaN vai dar true , ou seja, o NaN não é igual a NaN , e isso geralmente causa uma reação assim nos programadores:

Porque realmente não faz o menor sentido…

Mas! No nosso Map, se definirmos uma chave como sendo NaN , o valor dela será sim NaN , ou seja, ela acaba se resolvendo para ela mesma:

E isso é a apresentação de um corner case bizarro que acontece quando usamos Maps.

Maps e DOM

No ES5 tínhamos um grande problema quando precisávamos associar um elemento do DOM a alguma API. A abordagem padrão era criar um código extenso como o abaixo, que simplesmente retorna um objeto API com vários métodos que manipulam elementos do DOM, permitindo que a gente inclua ou remova eles do Cache e obter um objeto API para o elemento se ele existir:

No ES6, temos a grande vantagem de podermos indexar como chave os elementos do DOM, então podemos simplesmente adicionar o método relativo a ele em um valor como função.

O ganho aqui não é só de leitura, mas também de performance. Veja que todos os métodos (ou a maioria deles) agora só possuem uma linha de código, o que significa que podemos deixar eles de modo inline sem problemas, economizando espaço de requisição em uma aplicação front-end.

Outras coisas com Maps

Symbols

Os Maps são coleções de dados, as famosas collections que assombram todos os estudantes de algoritmos na faculdade. Isso significa que é fácil de buscar dentro delas se uma chave é existente ou não. Temos o caso exótico do NaN que mencionei acima, mas fora isso todos os objetos Symbol são tratados de forma diferente, então você vai precisar usar eles por valor:

Viu o que aconteceu? Symbol é um objeto único sempre, que retorna uma referência, então enquanto você guardar bem a referência que o Symbol te deu está tudo certo.

Key-Cast

Diferentemente do nosso modelo inicial aonde tínhamos somente chaves string, os maps permitem qualquer tipo de informação e não realizam nenhuma conversão destes tipos, a gente pode estar acostumado a transformar tudo em string, mas lembre-se de que isso não é mais necessário.

Clear

Podemos limpar um Map sem perder a referência para ele, porque ele é uma coleção, como já foi dito.

Entries e Iterator

Se você já leu meu artigo sobre iterators, você deve ter reconhecido o Map como um modelo de implementação deste protocolo. Portanto isso significa que você pode iterar pelo .entries() dele, mas se estamos usando o Map como um iterable, isso já vai ser feito de qualquer maneira, então você não precisa iterar explicitamente, veja que map[Symbol.iterator] === map.entries retornará true .

Justamente como o .entries() , o Map tem outros dois métodos que você pode tirar vantagem, o .keys() e o .values() . Então basicamente eles são auto explicativos…

Size

O Map também vem de fábrica com uma propriedade somente leitura chamada .size que te dá, a qualquer momento, a quantidade de pares no hash map. Se comporta basicamente igual ao Array.prototype.length .

Ordenação

Uma penúltima coisa que vale a pena mencionar é que os pares de um Map são iterados em ordem de inserção e não em ordem aleatória como era no Object.keys . Tanto que usávamos for ... in para iterar sobre as propriedades de um objeto de forma totalmente arbitrária.

Loop

Por fim, temos o método conhecido .forEach() que faz basicamente o que o método análogo do Array faz. Lembre-se de que não temos chaves como strings aqui.

Conclusão

Maps realmente vieram para salvar um pouco a nossa pele em relação a coleções de chaves e valores ou dicionários de dados que temos que manipular de forma muito complicada no ES5, porém é necessário tomar algum cuidado e também, principalmente, conhecer a estrutura que estamos trabalhando para não cairmos na falha de termos um objeto chave que não entendemos.

Espero que tenham gostado, visitem este artigo para saber um pouco mais, tirei muito conteúdo daí e também muitos exemplos :)

Like what you read? Give Lucas Santos a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.