Comprender Map y Set de Objetos en JavaScript
INFO
La fuente original (en ingles) de este tutorial se encuentra aquí
Introducción
En JavaScript, los desarrolladores suelen dedicar mucho tiempo a decidir la estructura de datos correcta a utilizar. Esto se debe a que elegir la estructura de datos correcta puede facilitar la manipulación de esos datos en el futuro, ahorrando tiempo y haciendo que el código sea más fácil de comprender. Las dos estructuras de datos predominantes para almacenar colecciones de datos son Objetos y Arrays (un tipo de objeto). Los desarrolladores utilizan objetos para almacenar pares clave/valor y matrices para almacenar listas indexadas. Sin embargo, para brindar a los desarrolladores más flexibilidad, la especificación ECMAScript 2015 introdujo dos nuevos tipos de objetos iterables: Mapṣ, que son colecciones ordenadas de pares clave/valor, y Sets, que son colecciones de valores únicos.
En este artículo, repasará los objetos Map y Set, qué los hace similares o diferentes a Objects y Arrays, las propiedades y métodos disponibles para ellos y ejemplos de algunos usos prácticos.
Maps
Un Map es una colección de pares clave/valor que puede utilizar cualquier tipo de datos como clave y puede mantener el orden de sus entradas. Los Maps tienen elementos tanto de Objects (una colección única de pares clave/valor) como de Arrays (una colección ordenada), pero conceptualmente son más similares a los Objects. Esto se debe a que, aunque el tamaño y el orden de las entradas se conservan como un Array, las entradas en sí son pares clave/valor como los Objects.
Los Maps se pueden inicializar con sintaxis new Map()
:
const map = new Map()
Esto nos da un Map vacío:
Output
Map(0) {}
Agregar Valores a un Map
Puede agregar valores a un Map con el método set()
. El primer argumento será la clave y el segundo argumento será el valor.
Lo siguiente agrega tres pares clave/valor al map
:
map.set('firstName', 'Luke')
map.set('lastName', 'Skywalker')
map.set('occupation', 'Jedi Knight')
Aquí comenzamos a ver cómo los _Map_s
tienen elementos tanto de Objects como de Arrays. Al igual que un Array, tenemos una colección indexada a cero y también podemos ver cuántos elementos hay en el _Map_
de forma predeterminada. Los _Map_s
usan la sintaxis =>
para indicar pares clave/valor como key => value
:
Output
Map(3)
0: {"firstName" => "Luke"}
1: {"lastName" => "Skywalker"}
2: {"occupation" => "Jedi Knight"}
Este ejemplo es similar a un objeto normal con claves basadas en cadenas, pero podemos usar cualquier tipo de datos como clave con Maps.
Además de configurar valores manualmente en un Map, también podemos inicializar un Map con valores. Hacemos esto usando una Array de Arrays que contiene dos elementos, cada uno de los cuales es un par clave/valor, que se ve así:
[ [ 'key1', 'value1'], ['key2', 'value2'] ]
Usando la siguiente sintaxis, podemos recrear el mismo Map:
const map = new Map([
['firstName', 'Luke'],
['lastName', 'Skywalker'],
['occupation', 'Jedi Knight'],
])
Nota
Este ejemplo utiliza comas finales, también conocidas como comas colgantes. Esta es una práctica de formato de JavaScript en la que el elemento final de una serie al declarar una colección de datos tiene una coma al final. Aunque esta opción de formato se puede utilizar para obtener diferencias más limpias y facilitar la manipulación del código, utilizarla o no es una cuestión de preferencia. Para obtener más información sobre las comas finales, consulte este artículo de las Comas Finales de los documentos web de MDN.
Por cierto, esta sintaxis es la misma que el resultado de llamar a Object.entries()
en un Object. Esto proporciona una forma preparada para convertir un Object en un Map, como se muestra en el siguiente bloque de código:
const luke = {
firstName: 'Luke',
lastName: 'Skywalker',
occupation: 'Jedi Knight',
}
const map = new Map(Object.entries(luke))
Alternativamente, puedes convertir un Map nuevamente en un Object o una Array con una sola línea de código.
Lo siguiente convierte un Map en un Object:
const obj = Object.fromEntries(map)
Esto dará como resultado el siguiente valor de obj
:
Output
{firstName: "Luke", lastName: "Skywalker", occupation: "Jedi Knight"}
Ahora, convertimos un Map en un Array:
const arr = Array.from(map)
Esto dará como resultado el siguiente Array para arr
:
Output
[ ['firstName', 'Luke'],
['lastName', 'Skywalker'],
['occupation', 'Jedi Knight'] ]
Claves de Map
Los Map aceptan cualquier tipo de datos como clave y no permiten valores de clave duplicados. Podemos demostrar esto creando un Map y usando valores que no sean cadenas como claves, además de establecer dos valores para la misma clave.
Primero, inicialicemos un Map con claves que no sean cadenas:
const map = new Map()
map.set('1', 'String one')
map.set(1, 'This will be overwritten')
map.set(1, 'Number one')
map.set(true, 'A Boolean')
Aunque es una creencia común que un objeto JavaScript normal ya puede manejar números, valores booleanos y otros tipos de datos primitivos como claves, en realidad este no es el caso, porque los objetos convierten todas las claves en cadenas.
Como ejemplo, inicialice un objeto con una clave numérica y compare el valor de una clave numérica 1
y una clave "1"
en cadena:
// Initialize an object with a numerical key
const obj = { 1: 'One' }
// The key is actually a string
obj[1] === obj['1'] // true
Es por eso que si intenta utilizar un Object como clave, en su lugar imprimirá la cadena object Object
.
Como ejemplo, cree un Object y luego utilícelo como clave de otro Object:
// Create an object
const objAsKey = { foo: 'bar' }
// Use this object as the key of another object
const obj = {
[objAsKey]: 'What will happen?'
}
Esto producirá lo siguiente:
Output
{[object Object]: "What will happen?"}
Este no es el caso de Map. Intente crear un Object y configurarlo como clave de un Map:
// Create an object
const objAsKey = { foo: 'bar' }
const map = new Map()
// Set this object as the key of a Map
map.set(objAsKey, 'What will happen?')
La clave del elemento Map ahora es el objeto que creamos.
Output
key: {foo: "bar"}
value: "What will happen?"
Hay una cosa importante a tener en cuenta sobre el uso de un Object o Array como clave: el Map usa la referencia al Object para comparar la igualdad, no el valor literal del Objeto. En JavaScript {} === {}
devuelve false
, porque los dos Object no son los mismos dos Object, a pesar de tener el mismo valor (vacío).
Eso significa que agregar dos Objects únicos con el mismo valor creará un Map con dos entradas:
// Add two unique but similar objects as keys to a Map
map.set({}, 'One')
map.set({}, 'Two')
Esto producirá lo siguiente:
Output
Map(2) {{…} => "One", {…} => "Two"}
Pero usar la misma referencia de Object dos veces creará un Map con una entrada.
// Add the same exact object twice as keys to a Map
const obj = {}
map.set(obj, 'One')
map.set(obj, 'Two')
Lo que resultará en lo siguiente:
Output
Map(1) {{…} => "Two"}
El segundo set()
actualiza exactamente la misma clave que el primero, por lo que terminamos con un Map que solo tiene un valor.
Obtener y Remover Elementos de un Map
Una de las desventajas de trabajar con Object es que puede resultar difícil enumerarlos o trabajar con todas las claves o valores. La estructura del Map, por el contrario, tiene muchas propiedades integradas que hacen que trabajar con sus elementos sea más directo.
Podemos inicializar un nuevo Map para demostrar los siguientes métodos y propiedades: delete()
, has()
, get()
y size
.
// Initialize a new Map
const map = new Map([
['animal', 'otter'],
['shape', 'triangle'],
['city', 'New York'],
['country', 'Bulgaria'],
])
Utilice el método has()
para comprobar la existencia de un elemento en un Map. has()
devolverá un valor Booleano.
// Check if a key exists in a Map
map.has('shark') // false
map.has('country') // true
Utilice el método get()
para recuperar un valor por clave.
// Get an item from a Map
map.get('animal') // "otter"
Un beneficio particular que tienen los Maps sobre los Objects es que puedes encontrar el tamaño de un Map en cualquier momento, como puedes hacerlo con un Array. Puede obtener el recuento de elementos en un Maps con la propiedad size
. Esto implica menos pasos que convertir un Object en un Array para encontrar la longitud.
// Get the count of items in a Map
map.size // 4
Utilice el método delete()
para eliminar un elemento de un Map mediante clave. El método devolverá un valor Booleano— true
si existía un elemento y se eliminó, y false
si no coincidía con ningún elemento.
// Delete an item from a Map by key
map.delete('city') // true
Esto dará como resultado el siguiente Map:
Output
Map(3) {"animal" => "otter", "shape" => "triangle", "country" => "Bulgaria"}
Finalmente, se pueden borrar todos los valores de un Map con map.clear()
.
// Empty a Map
map.clear()
Esto producirá:
Output
Map(0) {}
Claves, Valores y Entradas para Maps
Los Objects pueden recuperar claves, valores y entradas utilizando las propiedades del constructor Object
. Maps, por otro lado, tiene métodos prototipo que nos permiten obtener las claves, valores y entradas de la instancia de Map directamente.
Los métodos keys()
, values()
y entries()
devuelven un MapIterator
, que es similar a un Array en el sentido de que puedes usar for...of
para recorrer los valores.
Aquí hay otro ejemplo de un Map, que podemos usar para demostrar estos métodos:
const map = new Map([
[1970, 'bell bottoms'],
[1980, 'leg warmers'],
[1990, 'flannel'],
])
El método keys()
devuelve las claves:
map.keys()
Output
MapIterator {1970, 1980, 1990}
El método values()
devuelve los valores:
map.values()
Output
MapIterator {"bell bottoms", "leg warmers", "flannel"}
El método entries()
devuelve una matriz de pares clave/valor:
map.entries()
Output
MapIterator {1970 => "bell bottoms", 1980 => "leg warmers", 1990 => "flannel"}
Iteración con Map
Map tiene un método forEach
incorporado, similar a un Array, para iteración incorporada. Sin embargo, hay una pequeña diferencia en lo que iteran. La devolución de llamada forEach
de un Map itera a través del value
, la key
y el map
en sí, mientras que la versión Array itera a través del item
, el index
y el array
en sí.
// Map
Map.prototype.forEach((value, key, map) = () => {})
// Array
Array.prototype.forEach((item, index, array) = () => {})
Esta es una gran ventaja para los Maps sobre los Objects, ya que los Objects deben convertirse con keys()
, values()
o entries()
, y no existe una forma sencilla de recuperar las propiedades de un Object sin convertirlo.
Para demostrar esto, iteremos a través de nuestro Map y registremos los pares clave/valor en la consola:
// Log the keys and values of the Map with forEach
map.forEach((value, key) => {
console.log(`${key}: ${value}`)
})
Esto dará:
Output
1970: bell bottoms
1980: leg warmers
1990: flannel
Dado que un bucle for...of
itera sobre iterables como Map y Array, podemos obtener exactamente el mismo resultado desestructurando la matriz de elementos del Map:
// Destructure the key and value out of the Map item
for (const [key, value] of map) {
// Log the keys and values of the Map with for...of
console.log(`${key}: ${value}`)
}
Propiedades y Métodos del Map
La siguiente tabla muestra una lista de propiedades y métodos de Map para referencia rápida:
Propiedades/Métodos | Descripción | Devoluciones |
---|---|---|
set(key, value) | Agrega un par clave/valor a un Map | Map Object |
delete(key) | Elimina un par clave/valor de un Map por clave | Boolean |
get(key) | Devuelve un valor por clave | |
has(key) | Comprueba la presencia de un elemento en un Map por clave | Boolean |
clear() | Elimina todos los elementos de un Map | N/A |
keys() | Devuelve todas las claves en un Map | MapIterator object |
values() | Devuelve todos los valores en un Map | MapIterator object |
entries() | Devuelve todas las claves y valores en un Map como [key, value] | MapIterator object |
forEach() | Itera a través del Map en orden de inserción. | N/A |
size | Devuelve el número de elementos en un Map | Number |
Cuándo Utilizar Map
En resumen, los Maps son similares a los Objects en el sentido de que contienen pares clave/valor, pero los Maps tienen varias ventajas sobre los Objects:
- Tamaño - Los Maps tienen una propiedad
size
, mientras que los Objects no tienen una forma integrada de recuperar su tamaño. - Iteración - Los Maps se pueden iterar directamente, mientras que los Objects no.
- Flexibilidad - Los Maps pueden tener cualquier tipo de datos (primitivo u objeto) como clave de un valor, mientras que los Objects solo pueden tener cadenas.
- Ordenado - Los Maps conservan su orden de inserción, mientras que los Objects no tienen un orden garantizado.
Debido a estos factores, los Maps son una poderosa estructura de datos a considerar. Sin embargo, Objects también tiene algunas ventajas importantes:
- JSON - Los Objects funcionan perfectamente con
JSON.parse()
yJSON.stringify()
, dos funciones esenciales para trabajar con JSON, un formato de datos común con el que tratan muchas API REST. - Trabajar con un solo elemento - Al trabajar con un valor conocido en un Object, puede acceder a él directamente con la clave sin la necesidad de utilizar un método, como
get()
de Map.
Esta lista le ayudará a decidir si un Map u Object es la estructura de datos adecuada para su caso de uso.
Set
Un Set es una colección de valores únicos. A diferencia de un Map, un Set es conceptualmente más similar a un Array que a un Object, ya que es una lista de valores y no pares clave/valor. Sin embargo, Set no reemplaza a los Arrays, sino más bien un complemento para brindar soporte adicional para trabajar con datos duplicados.
Puede inicializar Set con la sintaxis new Set()
.
const set = new Set()
Esto nos da un Set vacío:
Output
Set(0) {}
Los elementos se pueden agregar a un Set con el método add()
. (Esto no debe confundirse con el método set()
disponible para Map, aunque son similares).
// Add items to a Set
set.add('Beethoven')
set.add('Mozart')
set.add('Chopin')
Dado que los Sets solo pueden contener valores únicos, se ignorará cualquier intento de agregar un valor que ya exista.
set.add('Chopin') // Set will still contain 3 unique values
Nota
La misma comparación de igualdad que se aplica a las claves del Map se aplica a los elementos del Set. Dos objetos que tengan el mismo valor pero no compartan la misma referencia no se considerarán iguales.
También puede inicializar Sets con un Array de valores. Si hay valores duplicados en la matriz, se eliminarán del Set.
// Initialize a Set from an Array
const set = new Set(['Beethoven', 'Mozart', 'Chopin', 'Chopin'])
Output
Set(3) {"Beethoven", "Mozart", "Chopin"}
Por el contrario, un Set se puede convertir en un Array con una línea de código:
const arr = [...set]
Output
(3) ["Beethoven", "Mozart", "Chopin"]
Set tiene muchos de los mismos métodos y propiedades que Map, incluidos delete()
, has()
, clear()
y size
.
// Delete an item
set.delete('Beethoven') // true
// Check for the existence of an item
set.has('Beethoven') // false
// Clear a Set
set.clear()
// Check the size of a Set
set.size // 0
Tenga en cuenta que Set no tiene una manera de acceder a un valor mediante una clave o índice, como Map.get(key) o arr[index].
Claves, Valores y Entradas para Sets
Tanto Map como Set tienen métodos keys()
, values()
y entries()
que devuelven un iterador. Sin embargo, si bien cada uno de estos métodos tiene un propósito distinto en Map, los Sets no tienen claves y, por lo tanto, las claves son un alias para los valores. Esto significa que keys()
y values()
devolverán el mismo iterador, y entries()
devolverá el valor dos veces. Tiene más sentido usar únicamente values()
con Set, ya que los otros dos métodos existen para mantener la coherencia y la compatibilidad cruzada con Map.
const set = new Set([1, 2, 3])
// Get the values of a set
set.values()
Output
SetIterator {1, 2, 3}
Iteración con Set
Al igual que Map, Set tiene un método forEach()
incorporado. Dado que los Sets no tienen claves, el primer y segundo parámetro de la devolución de llamada forEach()
devuelven el mismo valor, por lo que no hay ningún caso de uso fuera de la compatibilidad con Map. Los parámetros de forEach()
son value, key, set
.
Tanto forEach()
como for...of
se pueden utilizar en Set. Primero, veamos la iteración de forEach()
:
const set = new Set(['hi', 'hello', 'good day'])
// Iterate a Set with forEach
set.forEach((value) => console.log(value))
Entonces podemos escribir la versión for...of
:
// Iterate a Set with for...of
for (const value of set) {
console.log(value);
}
Ambas estrategias producirán lo siguiente:
Output
hi
hello
good day
Propiedades y Métodos de Set
La siguiente tabla muestra una lista de propiedades y métodos de Set para referencia rápida:
Propiedades/Métodos | Descripción | Devoluciones |
---|---|---|
add(value) | Agrega un nuevo elemento a un Set | Set Object |
delete(value) | Elimina el elemento especificado de un Set | Boolean |
has() | Comprueba la presencia de un elemento en un Set | Boolean |
clear() | Elimina todos los elementos de un Set | N/A |
keys() | Devuelve todos los valores de un Set (lo mismo que values() ) | SetIterator object |
values() | Devuelve todos los valores de un Set (lo mismo que keys() ) | SetIterator object |
entries() | Devuelve todos los valores en un Set como [value, value] | SetIterator object |
forEach() | Itera a través del Set en orden de inserción | N/A |
size | Devuelve el número de elementos de un Set | Number |
Cuándo utilizar el Set
Set es una adición útil a su kit de herramientas de JavaScript, particularmente para trabajar con valores duplicados en datos.
En una sola línea podemos crear un nuevo Array sin valores duplicados a partir de un Array que tiene valores duplicados.
const uniqueArray = [ ...new Set([1, 1, 2, 2, 2, 3])] // (3) [1, 2, 3]
Esto dará:
Output
(3) [1, 2, 3]
Set se puede utilizar para encontrar la unión, intersección y diferencia entre dos Sets de datos. Sin embargo, los Arrays tienen una ventaja significativa sobre los Set para la manipulación adicional de los datos debido a los métodos sort()
, map()
, filter()
y reduce()
, así como la compatibilidad directa con los métodos JSON
.
Conclusión
En este artículo, aprendió que un Map es una colección de pares clave/valor ordenados y que un Set es una colección de valores únicos. Ambas estructuras de datos agregan capacidades adicionales a JavaScript y simplifican tareas comunes, como encontrar la longitud de una colección de pares clave/valor y eliminar elementos duplicados de un conjunto de datos, respectivamente. Por otro lado, los Objects y Arrays se han utilizado tradicionalmente para el almacenamiento y manipulación de datos en JavaScript, y tienen compatibilidad directa con JSON, lo que los sigue convirtiendo en las estructuras de datos más esenciales, especialmente para trabajar con API REST. Los Maps y Sets son principalmente útiles como soporte de estructuras de datos para Objects y Arrays.