Seleccionar página

Siguiendo con el conjunto de artículos que estamos escribiendo con la excusa de desarrollar una función universal que compare cualquier tipo de dato en Javascript, hoy nos vamos a adentrar en las características de los objetos Map y Set.

Aunque han sido estandarizados con ES6, lo cierto es que muchos motores de Javascript han incluido estos objetos antes de que se cerrara el estándar, por lo que podemos encontrar implementaciones de Map y Set, aunque sean parciales, en navegadores como IE9 o IE11.

Estos objetos nos pueden ayudar a gestionar conjuntos de datos en muchas circunstancias y es interesante que sepamos cómo se utilizan y que pequeños secretos esconden. Vamos a revisarlos.

Map

El objeto Map es un diccionario clave-valor donde cualquier tipo puede ser usado como clave. Esta es la principal característica de los mapas y su mayor diferencia con los objetos, donde las claves sólo pueden ser cadenas de texto. En los objetos Map cualquier elemento, incluidos los objetos como matrices o funciones, pueden ser claves para localizar valores.

Los Map se utilizan con una sencilla interfaz donde los principales métodos son:

  • set( key, value ): añadir una nueva pareja clave-valor
  • get( key ): obtener el valor asociado a una clave
  • delete( key ): borrar una pareja clave-valor por medio de la clave.
  • has( key ): comprobar si hay una determinada clave en el mapa.

Aquí tenemos un breve ejemplo:

var map = new Map();

map.set( 'one', 1 );
map.set( 'two', 2 );
map.set( 'three', 3 );

console.log( "map.get('two') =", map.get('two') );

map.delete('three');

console.log( "map.has('three') =", map.has('three') );

Como hemos comentado, cuando se ve con mayor claridad su diferencia con respecto al uso de objetos es cuando utilizamos como clave valores que no son cadenas. Podemos utilizar números, valores booleanos, objetos, fechas, funciones, elementos del DOM, etc. De esta forma podemos asociar valores en un mapa a cualquier tipo de clave.

var obj = {
    a: 1,
    b: 2
};
function mult(a, b) {
    return a * b;
}

var map = new Map();

map.set( false, 0 );
map.set( obj, "is a object" );
map.set( mult, "function mult" );

console.log( map.get(mult) );

Tenemos que tener en cuenta que las claves se identifican dentro del mapa con igualdad estricta, es decir, igual que con ===. De esta forma, podremos obtener el valor asociado a un objeto sólo si pasamos exactamente ese objeto (no uno con iguales valores) al método get().

var obj1 = {
    a: 1,
    b: 2
};

var obj2 = {
    a: 1,
    b: 2
};

var map = new Map();
map.set( obj1, 1 );

console.log( 'map.has(obj1) = ', map.has(obj1) );    // true
console.log( 'map.has(obj2) = ', map.has(obj2) );    // false

Para completar esta breve introducción debemos conocer el resto de métodos y propiedades que tienen los objetos Map:

  • size: contiene el número de valores en el mapa.
  • forEach( (value, key, map) => {}, [thisValue] ): recorre todos elementos contenidos en el mapa.
  • values(): devuelve un iterable con los valores del mapa.
  • keys(): devuelve un iterable con las claves del mapa.
  • entries(): devuelve un iterable con matrices [key, value].
  • clear(): elimina todos los valores del mapa.

También debemos saber que Map se convierte automáticamente en un iterable si es utilizado dentro de un bucle for of, ya que se ha definido un método específico que responde al símbolo bien conocido @@iterable.

Por último, el constructor de Map puede aceptar una matriz -o un interable- con un conjunto de pares clave-valor para inicializar el objeto directamente en su creación.

var myMap = new Map( [ [0, "zero"], [1, "one"], [2, "two"] ] );

for (var [key, value] of myMap) {
    console.log(key + " = " + value);
}

Las oportunidades que ofrece el objeto Map son muy interesantes, su interfaz es sencilla y fácil de utilizar y puede ser un recurso de mucha utilidad a la hora de asociar información a cualquier elemento con el que estemos trabajando, ya que, como cualquier tipo de dato puede ser utilizado como clave, podemos asociar valores a objetos, elementos del DOM, matrices, funciones, etc.

Set

El objeto Set permite almacenar valores únicos de cualquier tipo, desde valores básicos hasta objetos, con el única limitación de que no pueden estar duplicados.

Cuando se introduce un valor en un objeto Set, se comprueba que no existe ya dentro de los valores almacenados. La comprobación de valores duplicados se realizar por medio de una igualdad estricta, es decir, igual que con ===, por lo que no habrá dos referencias al mismo objeto, pero sí podrán almacenarse objetos diferentes con iguales valores en sus propiedades.

Igual que en el caso anterior, los Set se utilizan con una sencilla interfaz donde los principales métodos son:

  • add( value ): añadir una nueva pareja clave-valor
  • delete( value ): borrar una valor.
  • has( value ): comprobar si existe un determinado valor.

Vamos a ver cómo funciona con un sencillo ejemplo:

var obj1 = {
    a: 1,
    b: 2
};

var obj2 = {
    a: 1,
    b: 2
};

var map = new Set();

map.add( obj1 );
map.add( obj2 );

console.log( 'map.has(obj1) = ', map.has(obj1) );
console.log( 'map.has(obj2) = ', map.has(obj2) );

Para completar la interfaz de los objetos Set debemos conocer el resto de métodos y propiedades que tiene:

  • size: contiene el número de valores.
  • forEach( (value, key, map) => {}, [thisValue] ): para recorrer todos elementos contenidos en el mapa.
  • values(): devuelve un iterable con los valores del mapa.
  • entries(): devuelve un iterable con matrices [value, value] (es equivalente al mismo método en Map, pero devuelve el valor dos veces).
  • clear(): elimina todos los valores del objeto Set.

Al igual que Map, Set se convierte automáticamente en un iterable si es utilizado dentro de un bucle for of, ya que se ha definido un método específico que responde al símbolo bien conocido @@iterable y el constructor de Set puede aceptar una matriz -o un interable- con valores para inicializar el objeto directamente en su creación.

Es un objeto muy útil para guardar cualquier tipo de dato con la seguridad de que no estará duplicado. Con una llamada a .has() podremos comprobar si el elemento ya existe en el objeto y no deberemos preocuparnos si por algún motivo intentamos añadir dos veces el mismo valor, ya que el método .add() no lanza ningún error si intentamos introducir un valor ya existente.

Comparar dos Map o Set y saber si son equivalentes

Está claro que si comparamos por medio de === sólo obtendremos un resultado verdadero si dos objetos Map o Set son exactamente el mismo objeto. Normalmente si comparamos dos objetos de tipo Map o Set es muy probable que realmente nos interese saber si ambos objetos contienen los mismos valores, no si son referencia exactamente al mismo objeto. Para eso debemos adaptar nuestra función equal(), ya que hemos ido desarrollando y explicado en los útimos artículos, pero que hasta ahora no tiene en cuenta este tipo de objeto.

Lo primero que tenemos que hacer es comprobar si el entorno donde se está ejecutando tiene soporte para los objetos Map o Set. Recordemos que en equal(), aunque tenemos que soportar todas las funcionalidades de ES6, debe ser capaz de funcionar sin problemas en cualquier entorno con soporte para ES5.1, lo que no podemos hacer algo como a instanceof Map sin saber si Map existe. Para ello definimos dos variables de la siguiente forma:

var MAP_SUPPORT = typeof Map !== 'undefined';
var SET_SUPPORT = typeof Set !== 'undefined';

Nota: no hemos definido estas variables como constantes (const) por que nuestro código debe funcionar en ES 5.1 y este tipo de definición no está soportada en esa versión de Javascript.

A partir de aquí podemos incluir las líneas adecuadas para la comparación de los contenidos de los objetos Map y Set:

if ((MAP_SUPPORT &&                              // Map
    a instanceof Map && a.keys && a.values &&
    b instanceof Map && b.keys && b.values) ||
    (SET_SUPPORT &&                              // Set
    a instanceof Set && a.values &&
    b instanceof Set && b.values))
{
    if (a.size !== b.size) {                     // Check size
        return NOT_EQUAL;
    }
    if (a.size > 0) {
        if (a instanceof Map && b instanceof Map) {
            aKeys = Array.from(a.keys()).sort();
            bKeys = Array.from(b.keys()).sort();
            i = aKeys.length;
            while (i--) {
                if (aKeys[i] !== bKeys[i]) {
                    return NOT_EQUAL;
                }
            }
        }
        if (check(Array.from(a.entries()).sort(), Array.from(b.entries()).sort())) {
            return VALUE_AND_TYPE;
        }
        return NOT_EQUAL;
    }
}

Vamos a ser sinceros, esta no es nuestra primera implementación. Nuestro primer desarrollo estaba plagado de errores de concepto. En primer lugar, comparábamos las claves y valores llamado recursivamente a equal, pero para que funcione correctamente las claves sólo se pueden comparar con comparación estricta. Si tenemos un objeto como clave, sólo podemos considerar que otro objeto Map es igual si la clave es exactamente ese mismo objeto, no uno con iguales propiedades.

También cometimos el error de no ordenar los resultados y si los valores se incorporaban a un objeto Map o Set en diferente orden se obtenía como resultado que no eran iguales (lo cual no es cierto), por lo que tuvimos que añadir .sort() para ordenar los resultados antes de hacer las comparaciones.

Hemos puesto aquí solo un esquema de funcionamiento. Si quieres puedes consultar el código completo en GitHub y el juego de pruebas que incluye este caso.

Conclusiones

Los objetos Map y Set ofrecen una funcionalidad muy sencilla y a la vez muy interesante. Durante mucho tiempo los programadores Javascript han desarrollado funcionalidades de este tipo por medio de objetos, matrices y programas auxiliares de todo tipo. Ahora disponemos de un soporte nativo, consistente y eficiente.

Como vemos, aunque parecía que nuestra función equal() ya era bastante completa, lo cierto es que no tenía en cuenta este tipo de objetos, que cada día son más importantes. Dar cobertura a todas las capacidades de ES6 nos hará continuar en los próximos artículos analizando algunas nuevas características del lenguaje. Si no puedes esperar, descarga la versión final de esqueal.js con npm.

Novedades

HTTP2 para programadores. Enviar mensajes del servidor al cliente con Server Sent Event (sin WebSockets)

HTTP2 para programadores. Enviar mensajes del servidor al cliente con Server Sent Event (sin WebSockets)

En esta charla, organizada por MadridJS, Pablo Almunia nos muestra cómo la mayoría de nosotros cuando oímos hablar por primera vez de HTTP2 nos ilusionamos con las posibilidades que presumiblemente se abrían para el desarrollo de soluciones web avanzadas y cómo muchos nos sentimos defraudados con lo que realmente se podía implementar.

En esta charla podemos ver cómo funciona el HTTP2, que debemos tener en cuenta en el servidor para hace uso de este protocolo y, sobre todo, cómo podemos enviar información desde el servidor al cliente de forma efectiva y fácil. Veremos con detenimiento cómo por medio de los Server-Sent Events (SSE) podemos recibir en el cliente datos enviados desde el servidor sin utilizar websocket, simplificando enormemente la construcción de aplicaciones con comunicación bidireccional.

Observables en Javascript con Proxies

Observables en Javascript con Proxies

En esta charla, organizada por MadridJS, Pablo Almunia nos habla de la observación reactiva de objetos en Javascript por medio de Proxy. Se describe paso a paso cómo funcionan los Proxies y en que casos pueden ser nuestro mejor aliado. Veremos que no hay que tenerles miedo, son bastante sencillos de utilizar, y nos ofrecen una gran abanico de posibilidades.

Aplicaciones JAMStack, SEO friendly y escalables con NextJS

Aplicaciones JAMStack, SEO friendly y escalables con NextJS

En esta charla de Madrid JS, Rafael Ventura nos describe las funcionalidades clave de NextJS, nos muestra en vivo cómo desarrollar una completa aplicación JAMStack con Server Side Rendering (SSR) y Static Site Generation (SSG) y termina mostrando como publicar esta aplicación en Vercel.

Stencil JS: mejora el Time To Market de tu producto, por Rubén Aguilera

Stencil JS: mejora el Time To Market de tu producto, por Rubén Aguilera

En esta charla Rubén Aguilera nos cuenta los problemas que tienen muchas empresas a la hora de sacar productos accesibles, vistosos y usables en el Time To Market que requiere Negocio y cómo podemos minimizar este tiempo gracias al DevUI con StencilJS para adecuar una aplicación de Angular a las exigencias del mercado en tiempo record.

breves

Descrubir algunas características de console

En el día a día nos encontramos muy a menudo utilizando console. Es una navaja multiusos que nos facilita la vida a la hora de depurar nuestro código. La mayoría de nosotros ha utilizado console.log(), pero tiene otras muchas funcionalidades.

Matrices dispersas o sparse arrays en JS

Una característica que puede producir algunos problemas, si no lo tenemos en cuenta, es la posibilidad de tener matrices con huecos, es decir, con algunos de sus elementos sin definir. Es lo que se suele denominar una matriz dispersa o sparse array. Veamos cómo trabajar con esta características de las matrices.

Operadores de bits usados con asiduidad

Cada día más a menudo podemos encontrar operadores binarios utilizados como formas abreviadas de algunas operaciones que de otra forma sería algo menos compactas y, quizás, más comprensibles. Veamos algunos casos en detalle.