JavaScript

JavaScript

Tutorial JavaScript: Mapas con Map

JavaScript mapas: creación y uso efectivo. Aprende a crear y usar mapas en JavaScript con ejemplos prácticos y detallados.

Aprende JavaScript y certifícate

Introducción a Map como estructura clave-valor avanzada

El objeto Map es una estructura de datos introducida en ECMAScript 2015 (ES6) que permite almacenar pares clave-valor donde tanto las claves como los valores pueden ser de cualquier tipo. A diferencia de los objetos tradicionales de JavaScript, que han sido históricamente utilizados como mapas, la estructura Map ofrece ventajas significativas que la convierten en una opción más robusta y flexible para muchos escenarios.

Diferencias entre Map y objetos tradicionales

Los objetos tradicionales de JavaScript presentan ciertas limitaciones cuando se utilizan como mapas:

  • Las claves de un objeto solo pueden ser strings o symbols
  • No mantienen el orden de inserción de manera consistente
  • No tienen métodos nativos para obtener su tamaño
  • No son iterables de forma directa

El objeto Map, por el contrario, resuelve estas limitaciones:

// Objeto tradicional como mapa (limitado)
const userRoles = {
  "john": "admin",
  "sarah": "editor"
};

// Map como estructura clave-valor avanzada
const userRolesMap = new Map();
userRolesMap.set("john", "admin");
userRolesMap.set("sarah", "editor");

Características principales de Map

1. Claves de cualquier tipo: Un Map puede utilizar cualquier valor como clave, incluyendo objetos, funciones y valores primitivos.

const map = new Map();

// Usando diferentes tipos como claves
map.set("stringKey", "Valor con clave string");
map.set(42, "Valor con clave numérica");
map.set(true, "Valor con clave booleana");

// Usando objetos como claves (imposible con objetos normales)
const userObject = { id: 1, name: "Alice" };
map.set(userObject, "Datos del usuario");

// Usando funciones como claves
const someFunction = () => console.log("Hello");
map.set(someFunction, "Función de saludo");

2. Orden de inserción preservado: Los Map mantienen el orden en que se insertaron los elementos, lo que facilita la iteración predecible.

const orderedMap = new Map();
orderedMap.set("primero", 1);
orderedMap.set("segundo", 2);
orderedMap.set("tercero", 3);

// La iteración respeta el orden de inserción
for (const [key, value] of orderedMap) {
  console.log(`${key}: ${value}`);
}
// Resultado:
// primero: 1
// segundo: 2
// tercero: 3

Rendimiento optimizado: Los Map están optimizados para operaciones frecuentes de adición y eliminación de elementos.

API intuitiva: Proporciona métodos claros para operaciones comunes como set(), get(), has(), delete() y clear().

Creación de un Map

Existen varias formas de crear un Map:

1. Constructor vacío:

const emptyMap = new Map();

2. A partir de un array de pares clave-valor:

const mapFromArray = new Map([
  ["key1", "value1"],
  ["key2", "value2"],
  [42, "answer"]
]);

3. Clonando otro Map:

const originalMap = new Map([["a", 1], ["b", 2]]);
const clonedMap = new Map(originalMap);

Comparación de claves en Map

Una característica fundamental de Map es cómo compara las claves para determinar su igualdad. A diferencia de los objetos que convierten las claves a strings, Map utiliza el algoritmo SameValueZero, similar a === pero con una diferencia importante: considera que NaN es igual a sí mismo.

const map = new Map();

// En objetos normales, diferentes objetos con el mismo contenido son claves diferentes
const obj1 = { id: 1 };
const obj2 = { id: 1 };

map.set(obj1, "Valor para objeto 1");
map.set(obj2, "Valor para objeto 2");

console.log(map.size); // 2, porque son objetos diferentes en memoria

// NaN como clave
map.set(NaN, "Valor para NaN");
console.log(map.get(NaN)); // "Valor para NaN"

Propiedades y métodos principales

El objeto Map proporciona una API rica para manipular los datos:

  • size: Propiedad que devuelve el número de elementos
  • set(key, value): Añade o actualiza un elemento
  • get(key): Recupera el valor asociado a una clave
  • has(key): Comprueba si existe una clave
  • delete(key): Elimina un elemento
  • clear(): Elimina todos los elementos
const inventory = new Map();

// Añadir elementos
inventory.set("apples", 5);
inventory.set("bananas", 8);
inventory.set("oranges", 12);

console.log(inventory.size); // 3

// Comprobar existencia
console.log(inventory.has("apples")); // true
console.log(inventory.has("mangoes")); // false

// Obtener valor
console.log(inventory.get("bananas")); // 8
console.log(inventory.get("mangoes")); // undefined (clave no existente)

// Eliminar elemento
inventory.delete("oranges");
console.log(inventory.size); // 2

// Limpiar el mapa
inventory.clear();
console.log(inventory.size); // 0

Casos de uso ideales para Map

Los Map son particularmente útiles en los siguientes escenarios:

  • Caché de datos: Cuando necesitas almacenar resultados de operaciones costosas.
  • Relaciones entre objetos: Cuando necesitas asociar datos a objetos sin modificarlos.
  • Almacenamiento de metadatos: Para asociar información adicional a estructuras existentes.
  • Contadores de frecuencia: Para llevar un registro de ocurrencias.
// Ejemplo: Caché de resultados de función
const functionResultCache = new Map();

function expensiveOperation(input) {
  // Si ya calculamos este valor, lo devolvemos de la caché
  if (functionResultCache.has(input)) {
    console.log("Returning cached result");
    return functionResultCache.get(input);
  }
  
  // Simulamos una operación costosa
  console.log("Calculating result...");
  const result = input * input * input; // Cálculo de ejemplo
  
  // Guardamos en caché para futuras llamadas
  functionResultCache.set(input, result);
  return result;
}

console.log(expensiveOperation(4)); // Calcula y guarda en caché
console.log(expensiveOperation(4)); // Usa el valor cacheado

El objeto Map representa un avance significativo en las estructuras de datos de JavaScript, ofreciendo una alternativa más potente y flexible a los objetos tradicionales para el almacenamiento de pares clave-valor. Su capacidad para utilizar cualquier tipo de dato como clave, junto con su API intuitiva y su rendimiento optimizado, lo convierten en una herramienta esencial en el arsenal de cualquier desarrollador de JavaScript moderno.

Técnicas avanzadas y casos de uso específicos

El objeto Map en JavaScript va más allá de las operaciones básicas, ofreciendo capacidades avanzadas que lo convierten en una herramienta extremadamente versátil para escenarios complejos. En esta sección exploraremos técnicas sofisticadas y patrones de uso que aprovechan todo el potencial de esta estructura de datos.

Encadenamiento de operaciones

Una técnica elegante con Map es el encadenamiento de métodos, que permite realizar múltiples operaciones en una sola expresión, mejorando la legibilidad y concisión del código:

const userPreferences = new Map()
  .set("theme", "dark")
  .set("fontSize", 14)
  .set("notifications", true)
  .set("language", "es");

console.log(userPreferences.get("theme")); // "dark"

Transformación de datos con Map

Los objetos Map pueden utilizarse para crear transformaciones bidireccionales entre conjuntos de datos relacionados:

// Mapa bidireccional para conversiones de unidades
const celsiusToFahrenheit = new Map();
const fahrenheitToCelsius = new Map();

// Poblamos los mapas con valores de conversión
for (let c = -20; c <= 40; c += 5) {
  const f = (c * 9/5) + 32;
  celsiusToFahrenheit.set(c, f);
  fahrenheitToCelsius.set(f, c);
}

console.log(`20°C = ${celsiusToFahrenheit.get(20)}°F`); // "20°C = 68°F"
console.log(`68°F = ${fahrenheitToCelsius.get(68)}°C`); // "68°F = 20°C"

Composición de Maps

Podemos crear estructuras de datos compuestas utilizando Map como elementos de otro Map, lo que permite modelar relaciones jerárquicas complejas:

// Sistema de gestión de usuarios y permisos
const userRoles = new Map();
const rolePermissions = new Map();

// Definimos permisos para cada rol
rolePermissions.set("admin", new Map([
  ["dashboard", true],
  ["users", true],
  ["settings", true],
  ["billing", true]
]));

rolePermissions.set("editor", new Map([
  ["dashboard", true],
  ["users", false],
  ["settings", false],
  ["content", true]
]));

// Asignamos roles a usuarios
userRoles.set("alice@example.com", "admin");
userRoles.set("bob@example.com", "editor");

// Función para verificar permisos
function hasPermission(user, permission) {
  const role = userRoles.get(user);
  if (!role) return false;
  
  const permissions = rolePermissions.get(role);
  return permissions ? permissions.get(permission) || false : false;
}

console.log(hasPermission("alice@example.com", "users")); // true
console.log(hasPermission("bob@example.com", "users")); // false

Implementación de caché con expiración

Podemos crear una caché con tiempo de expiración utilizando Map y sus capacidades para almacenar cualquier tipo de valor:

class ExpiringCache {
  constructor(defaultTTL = 60000) {
    this.cache = new Map();
    this.defaultTTL = defaultTTL; // Tiempo de vida predeterminado en ms
  }

  set(key, value, ttl = this.defaultTTL) {
    const expiry = Date.now() + ttl;
    this.cache.set(key, { value, expiry });
    return this;
  }

  get(key) {
    const item = this.cache.get(key);
    
    // Si no existe o ha expirado, retornamos undefined
    if (!item || Date.now() > item.expiry) {
      if (item) this.cache.delete(key); // Limpiamos entradas expiradas
      return undefined;
    }
    
    return item.value;
  }

  has(key) {
    const item = this.cache.get(key);
    const valid = item && Date.now() <= item.expiry;
    if (item && !valid) this.cache.delete(key);
    return valid;
  }

  // Método para limpiar entradas expiradas
  cleanup() {
    const now = Date.now();
    for (const [key, item] of this.cache) {
      if (now > item.expiry) {
        this.cache.delete(key);
      }
    }
  }
}

// Uso
const cache = new ExpiringCache(5000); // 5 segundos TTL
cache.set("apiData", { results: [1, 2, 3] });
cache.set("shortLived", "Expiro rápido", 2000); // 2 segundos TTL

console.log(cache.get("apiData")); // { results: [1, 2, 3] }

// Después de 3 segundos
setTimeout(() => {
  console.log(cache.get("shortLived")); // undefined (expirado)
  console.log(cache.get("apiData")); // { results: [1, 2, 3] } (aún válido)
}, 3000);

Implementación de un sistema de eventos

Los Map son ideales para implementar un sistema de eventos personalizado gracias a su capacidad para almacenar funciones como valores:

class EventEmitter {
  constructor() {
    this.events = new Map();
  }

  on(eventName, callback) {
    if (!this.events.has(eventName)) {
      this.events.set(eventName, new Set());
    }
    this.events.get(eventName).add(callback);
    return this;
  }

  off(eventName, callback) {
    const callbacks = this.events.get(eventName);
    if (callbacks) {
      callbacks.delete(callback);
      if (callbacks.size === 0) {
        this.events.delete(eventName);
      }
    }
    return this;
  }

  emit(eventName, ...args) {
    const callbacks = this.events.get(eventName);
    if (callbacks) {
      for (const callback of callbacks) {
        callback(...args);
      }
    }
    return this;
  }

  once(eventName, callback) {
    const onceWrapper = (...args) => {
      callback(...args);
      this.off(eventName, onceWrapper);
    };
    return this.on(eventName, onceWrapper);
  }
}

// Uso
const bus = new EventEmitter();

function messageHandler(msg) {
  console.log(`Mensaje recibido: ${msg}`);
}

bus.on("message", messageHandler)
   .once("init", () => console.log("Inicializado - solo se ejecuta una vez"));

bus.emit("message", "Hola mundo"); // "Mensaje recibido: Hola mundo"
bus.emit("init", "Sistema listo"); // "Inicializado - solo se ejecuta una vez"
bus.emit("init", "Reinicio"); // No hace nada, el evento ya se ejecutó

bus.off("message", messageHandler);
bus.emit("message", "Este mensaje no se mostrará");

Implementación de grafos con Map

Los objetos Map son excelentes para implementar estructuras de grafo donde necesitamos mapear nodos a sus conexiones:

class Graph {
  constructor(directed = false) {
    this.directed = directed;
    this.nodes = new Map();
  }

  addNode(node) {
    if (!this.nodes.has(node)) {
      this.nodes.set(node, new Set());
    }
    return this;
  }

  addEdge(node1, node2, weight = 1) {
    this.addNode(node1);
    this.addNode(node2);
    
    this.nodes.get(node1).add({ node: node2, weight });
    
    if (!this.directed) {
      this.nodes.get(node2).add({ node: node1, weight });
    }
    
    return this;
  }

  getNeighbors(node) {
    return this.nodes.get(node) || new Set();
  }

  hasPath(start, end, visited = new Set()) {
    if (start === end) return true;
    
    if (visited.has(start)) return false;
    visited.add(start);
    
    const neighbors = this.getNeighbors(start);
    for (const { node: neighbor } of neighbors) {
      if (this.hasPath(neighbor, end, visited)) {
        return true;
      }
    }
    
    return false;
  }
}

// Uso: Modelar una red de ciudades
const cityNetwork = new Graph();

cityNetwork
  .addEdge("Madrid", "Barcelona", 600)
  .addEdge("Madrid", "Valencia", 350)
  .addEdge("Barcelona", "Valencia", 350)
  .addEdge("Valencia", "Sevilla", 650)
  .addEdge("Sevilla", "Málaga", 200);

console.log(cityNetwork.hasPath("Madrid", "Málaga")); // true
console.log(cityNetwork.hasPath("Barcelona", "Málaga")); // true

Uso de WeakMap para datos privados

WeakMap es una variante de Map que permite que sus claves sean recolectadas por el garbage collector cuando no hay otras referencias a ellas. Es ideal para implementar datos privados en clases:

// Implementación de propiedades privadas con WeakMap
const _private = new WeakMap();

class User {
  constructor(name, email) {
    _private.set(this, {
      name,
      email,
      loginAttempts: 0,
      lastLogin: null
    });
  }

  get name() {
    return _private.get(this).name;
  }

  get email() {
    // Ocultamos parte del email para privacidad
    const email = _private.get(this).email;
    const [username, domain] = email.split('@');
    return `${username.charAt(0)}${'*'.repeat(username.length - 2)}${username.charAt(username.length - 1)}@${domain}`;
  }

  login(password) {
    const data = _private.get(this);
    
    // Simulamos verificación de contraseña
    const isValidPassword = password.length > 0; // Simplificado para el ejemplo
    
    if (isValidPassword) {
      data.lastLogin = new Date();
      data.loginAttempts = 0;
      return true;
    } else {
      data.loginAttempts++;
      return false;
    }
  }

  get isLocked() {
    return _private.get(this).loginAttempts >= 3;
  }
}

const user = new User("Alice Smith", "alice.smith@example.com");
console.log(user.name); // "Alice Smith"
console.log(user.email); // "a****h@example.com"

console.log(user.login("correctpassword")); // true
console.log(user.login("")); // false
console.log(user.login("")); // false
console.log(user.login("")); // false
console.log(user.isLocked); // true

// No podemos acceder directamente a los datos privados
console.log(user._private); // undefined

Implementación de un LRU Cache (Least Recently Used)

Un caso de uso avanzado es implementar un caché LRU (Least Recently Used), que mantiene un número limitado de elementos y descarta los menos usados recientemente:

class LRUCache {
  constructor(capacity) {
    this.capacity = capacity;
    this.cache = new Map();
  }

  get(key) {
    if (!this.cache.has(key)) return undefined;
    
    // Obtenemos el valor
    const value = this.cache.get(key);
    
    // Refrescamos la entrada moviéndola al final (más reciente)
    this.cache.delete(key);
    this.cache.set(key, value);
    
    return value;
  }

  put(key, value) {
    // Si la clave ya existe, la eliminamos primero
    if (this.cache.has(key)) {
      this.cache.delete(key);
    }
    // Si alcanzamos la capacidad, eliminamos la entrada más antigua
    else if (this.cache.size >= this.capacity) {
      // Map.keys().next() nos da la primera clave (la más antigua)
      const oldestKey = this.cache.keys().next().value;
      this.cache.delete(oldestKey);
    }
    
    // Añadimos la nueva entrada
    this.cache.set(key, value);
  }
}

// Uso
const pageCache = new LRUCache(3);

pageCache.put("home", "<html>Home page</html>");
pageCache.put("about", "<html>About page</html>");
pageCache.put("contact", "<html>Contact page</html>");

console.log(pageCache.get("home")); // "<html>Home page</html>"

// Añadimos una nueva página, lo que debería expulsar la más antigua (about)
pageCache.put("products", "<html>Products page</html>");

console.log(pageCache.get("about")); // undefined (expulsada del caché)
console.log(pageCache.get("contact")); // "<html>Contact page</html>"

Uso de Map para memoización de funciones

La memoización es una técnica de optimización que almacena resultados de llamadas a funciones costosas. Map es ideal para implementar esta técnica:

function memoize(fn) {
  const cache = new Map();
  
  return function(...args) {
    // Usamos JSON.stringify para crear una clave única basada en los argumentos
    const key = JSON.stringify(args);
    
    if (cache.has(key)) {
      console.log("Resultado en caché");
      return cache.get(key);
    }
    
    console.log("Calculando resultado");
    const result = fn.apply(this, args);
    cache.set(key, result);
    return result;
  };
}

// Función costosa: cálculo de números de Fibonacci
function fibonacci(n) {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

// Versión memoizada
const memoizedFibonacci = memoize(function(n) {
  if (n <= 1) return n;
  return memoizedFibonacci(n - 1) + memoizedFibonacci(n - 2);
});

console.time("Sin memoización");
console.log(fibonacci(35)); // Muy lento
console.timeEnd("Sin memoización");

console.time("Con memoización");
console.log(memoizedFibonacci(35)); // Mucho más rápido
console.timeEnd("Con memoización");

Implementación de un sistema de observadores (Observer Pattern)

Map facilita la implementación del patrón Observer, permitiendo a los objetos suscribirse a cambios en otros objetos:

class Observable {
  constructor() {
    this.observers = new Map();
  }

  subscribe(event, observer) {
    if (!this.observers.has(event)) {
      this.observers.set(event, new Set());
    }
    this.observers.get(event).add(observer);
    
    // Devolvemos una función para facilitar la cancelación
    return () => this.unsubscribe(event, observer);
  }

  unsubscribe(event, observer) {
    const observers = this.observers.get(event);
    if (observers) {
      observers.delete(observer);
      if (observers.size === 0) {
        this.observers.delete(event);
      }
    }
  }

  notify(event, data) {
    const observers = this.observers.get(event);
    if (observers) {
      observers.forEach(observer => {
        if (typeof observer === 'function') {
          observer(data);
        } else if (observer && typeof observer.update === 'function') {
          observer.update(event, data);
        }
      });
    }
  }
}

// Uso
class DataStore extends Observable {
  constructor() {
    super();
    this.data = new Map();
  }

  setItem(key, value) {
    const oldValue = this.data.get(key);
    this.data.set(key, value);
    this.notify('change', { key, oldValue, newValue: value });
  }

  getItem(key) {
    return this.data.get(key);
  }
}

const store = new DataStore();

// Suscribimos un observador
const unsubscribe = store.subscribe('change', ({ key, oldValue, newValue }) => {
  console.log(`Valor cambiado: ${key}`);
  console.log(`  De: ${oldValue}`);
  console.log(`  A: ${newValue}`);
});

store.setItem('user', { name: 'John' });
store.setItem('user', { name: 'John', role: 'admin' });

// Cancelamos la suscripción
unsubscribe();

// Este cambio no generará notificaciones
store.setItem('status', 'active');

Los objetos Map en JavaScript ofrecen una flexibilidad excepcional para implementar estructuras de datos complejas y patrones de diseño avanzados. Su capacidad para manejar claves de cualquier tipo, combinada con su rendimiento optimizado, los convierte en una herramienta fundamental para resolver problemas sofisticados de programación. Dominar estas técnicas avanzadas permite a los desarrolladores crear soluciones elegantes y eficientes para una amplia gama de escenarios.

Recorrido e iteración de la estructura Map

Una de las ventajas fundamentales del objeto Map en JavaScript es su capacidad de ser recorrido e iterado de manera eficiente y flexible. A diferencia de los objetos tradicionales, Map implementa la interfaz iterable, lo que permite utilizar diversos métodos modernos para acceder y manipular sus elementos.

Métodos de iteración nativos

El objeto Map proporciona varios métodos integrados que devuelven iteradores, facilitando el recorrido de sus elementos:

  • keys(): Devuelve un iterador con las claves del mapa.
  • values(): Devuelve un iterador con los valores del mapa.
  • entries(): Devuelve un iterador con pares [clave, valor].
const userScores = new Map([
  ["alice", 95],
  ["bob", 82],
  ["carol", 90],
  ["dave", 75]
]);

// Iterando sobre las claves
for (const user of userScores.keys()) {
  console.log(`Usuario: ${user}`);
}
// Usuario: alice
// Usuario: bob
// Usuario: carol
// Usuario: dave

// Iterando sobre los valores
for (const score of userScores.values()) {
  console.log(`Puntuación: ${score}`);
}
// Puntuación: 95
// Puntuación: 82
// Puntuación: 90
// Puntuación: 75

// Iterando sobre las entradas (pares clave-valor)
for (const entry of userScores.entries()) {
  console.log(`${entry[0]}: ${entry[1]}`);
}
// alice: 95
// bob: 82
// carol: 90
// dave: 75

Desestructuración en bucles for...of

Una técnica elegante y moderna es utilizar la desestructuración de arrays en combinación con el bucle for...of para acceder directamente a las claves y valores:

const inventory = new Map([
  ["laptops", 25],
  ["phones", 50],
  ["tablets", 30],
  ["accessories", 120]
]);

// Desestructuración para acceder directamente a clave y valor
for (const [product, quantity] of inventory) {
  console.log(`${product}: ${quantity} unidades`);
}
// laptops: 25 unidades
// phones: 50 unidades
// tablets: 30 unidades
// accessories: 120 unidades

Este enfoque es posible porque Map utiliza entries() como su iterador predeterminado, lo que significa que iterar directamente sobre un Map es equivalente a iterar sobre map.entries().

Uso del método forEach

El método forEach ofrece una alternativa concisa para recorrer todos los elementos de un Map:

const employeeRoles = new Map([
  ["john", "developer"],
  ["sarah", "designer"],
  ["mike", "manager"],
  ["anna", "developer"]
]);

// forEach recibe una función callback con parámetros: valor, clave, mapa
employeeRoles.forEach((role, name, map) => {
  console.log(`${name} trabaja como ${role}`);
  // También podríamos usar el parámetro map si necesitáramos acceder al mapa completo
});
// john trabaja como developer
// sarah trabaja como designer
// mike trabaja como manager
// anna trabaja como developer

Observa que el orden de los parámetros en el callback de forEach es (valor, clave, mapa), lo cual es diferente al orden en los arrays (donde es (valor, índice, array)). Esto puede ser confuso al principio, pero tiene sentido considerando que en un Map los valores son generalmente más importantes que las claves.

Conversión a arrays

A menudo es útil convertir partes de un **Map** en arrays para aprovechar los métodos de array de JavaScript:

const productPrices = new Map([
  ["keyboard", 49.99],
  ["mouse", 29.99],
  ["monitor", 199.99],
  ["headset", 59.99]
]);

// Convertir claves a array
const products = Array.from(productPrices.keys());
console.log(products); 
// ["keyboard", "mouse", "monitor", "headset"]

// Convertir valores a array
const prices = Array.from(productPrices.values());
console.log(prices); 
// [49.99, 29.99, 199.99, 59.99]

// Convertir entradas a array
const entries = Array.from(productPrices.entries());
console.log(entries); 
// [["keyboard", 49.99], ["mouse", 29.99], ["monitor", 199.99], ["headset", 59.99]]

// Alternativa más concisa usando el operador spread
const productsSpread = [...productPrices.keys()];
const pricesSpread = [...productPrices.values()];
const entriesSpread = [...productPrices];

Filtrado y transformación de Maps

Podemos combinar la conversión a arrays con métodos como filter y map para crear nuevos Map con elementos filtrados o transformados:

const studentGrades = new Map([
  ["Ana", 95],
  ["Carlos", 68],
  ["Elena", 82],
  ["David", 75],
  ["Sofía", 91]
]);

// Filtrar estudiantes con calificaciones superiores a 80
const highPerformers = new Map(
  [...studentGrades].filter(([_, grade]) => grade > 80)
);
console.log([...highPerformers.keys()]); 
// ["Ana", "Elena", "Sofía"]

// Transformar calificaciones numéricas a letras
const gradeScale = new Map([
  [90, "A"],
  [80, "B"],
  [70, "C"],
  [60, "D"],
  [0, "F"]
]);

function getLetterGrade(numericGrade) {
  // Encontrar la calificación de letra correspondiente
  for (const [threshold, letter] of gradeScale) {
    if (numericGrade >= threshold) {
      return letter;
    }
  }
  return "F";
}

const letterGrades = new Map(
  [...studentGrades].map(([student, grade]) => [student, getLetterGrade(grade)])
);

console.log([...letterGrades]); 
// [["Ana", "A"], ["Carlos", "D"], ["Elena", "B"], ["David", "C"], ["Sofía", "A"]]

Iteración condicional y búsqueda

A veces necesitamos encontrar elementos específicos o detener la iteración cuando se cumple cierta condición:

const userLastLogin = new Map([
  ["user1", new Date("2023-10-15")],
  ["user2", new Date("2023-09-20")],
  ["user3", new Date("2023-10-18")],
  ["user4", new Date("2023-08-05")]
]);

// Encontrar usuarios que no han iniciado sesión en los últimos 30 días
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);

const inactiveUsers = [];
for (const [user, lastLogin] of userLastLogin) {
  if (lastLogin < thirtyDaysAgo) {
    inactiveUsers.push(user);
  }
}
console.log(inactiveUsers); // ["user2", "user4"]

// Encontrar el primer usuario que inició sesión después de una fecha específica
const targetDate = new Date("2023-10-16");
let firstRecentUser = null;

for (const [user, lastLogin] of userLastLogin) {
  if (lastLogin > targetDate) {
    firstRecentUser = user;
    break; // Detenemos la iteración al encontrar el primer resultado
  }
}
console.log(firstRecentUser); // "user3"

Iteración anidada con Maps compuestos

Cuando trabajamos con estructuras anidadas de Map, podemos utilizar bucles anidados para recorrer todos los niveles:

// Mapa de departamentos y sus empleados con roles
const companyStructure = new Map([
  ["Desarrollo", new Map([
    ["Alice", "Senior Developer"],
    ["Bob", "Junior Developer"],
    ["Charlie", "DevOps Engineer"]
  ])],
  ["Diseño", new Map([
    ["Diana", "UI Designer"],
    ["Eva", "UX Researcher"]
  ])],
  ["Marketing", new Map([
    ["Frank", "Marketing Manager"],
    ["Grace", "Content Creator"],
    ["Henry", "SEO Specialist"]
  ])]
]);

// Recorrer la estructura completa
for (const [department, employees] of companyStructure) {
  console.log(`\nDepartamento: ${department}`);
  console.log("------------------------");
  
  for (const [name, role] of employees) {
    console.log(`${name}: ${role}`);
  }
}
// Departamento: Desarrollo
// ------------------------
// Alice: Senior Developer
// Bob: Junior Developer
// Charlie: DevOps Engineer
// 
// Departamento: Diseño
// ------------------------
// Diana: UI Designer
// Eva: UX Researcher
// 
// Departamento: Marketing
// ------------------------
// Frank: Marketing Manager
// Grace: Content Creator
// Henry: SEO Specialist

// Contar el total de empleados por departamento
const employeeCount = new Map();

for (const [department, employees] of companyStructure) {
  employeeCount.set(department, employees.size);
}

console.log([...employeeCount]); 
// [["Desarrollo", 3], ["Diseño", 2], ["Marketing", 3]]

Patrones avanzados de iteración

Iteración con generadores

Los generadores pueden proporcionar formas personalizadas y eficientes de recorrer estructuras de datos complejas:

function* filterMapEntries(map, predicate) {
  for (const [key, value] of map) {
    if (predicate(value, key)) {
      yield [key, value];
    }
  }
}

const temperatures = new Map([
  ["Madrid", 28],
  ["París", 22],
  ["Londres", 18],
  ["Roma", 32],
  ["Berlín", 24]
]);

// Usar el generador para filtrar ciudades con temperaturas superiores a 25°C
const hotCities = new Map(filterMapEntries(temperatures, temp => temp > 25));
console.log([...hotCities]); // [["Madrid", 28], ["Roma", 32]]

Iteración asíncrona

Para operaciones que involucran promesas o llamadas asíncronas, podemos utilizar for-await-of con generadores asíncronos:

// Simulación de una API que devuelve datos de usuario
async function fetchUserData(userId) {
  // Simulamos una llamada a API con un retraso aleatorio
  await new Promise(resolve => setTimeout(resolve, 100 + Math.random() * 400));
  
  // Datos de ejemplo
  const users = {
    "user1": { name: "Alice Johnson", email: "alice@example.com" },
    "user2": { name: "Bob Smith", email: "bob@example.com" },
    "user3": { name: "Carol Davis", email: "carol@example.com" }
  };
  
  return users[userId] || { error: "Usuario no encontrado" };
}

async function* fetchAllUserData(userIds) {
  for (const id of userIds) {
    const userData = await fetchUserData(id);
    yield [id, userData];
  }
}

// Uso del generador asíncrono
async function processUsers() {
  const userIds = ["user1", "user2", "user3", "user4"];
  const userDataMap = new Map();
  
  // Iteración asíncrona
  for await (const [id, data] of fetchAllUserData(userIds)) {
    userDataMap.set(id, data);
    console.log(`Datos obtenidos para ${id}`);
  }
  
  console.log("Mapa de usuarios completo:", userDataMap);
}

processUsers();

Rendimiento y consideraciones prácticas

Al iterar sobre Map, es importante tener en cuenta algunas consideraciones de rendimiento:

  • Orden de iteración: Map garantiza que el orden de iteración sea el mismo que el orden de inserción, lo que puede ser crucial para ciertos algoritmos.
  • Modificación durante la iteración: A diferencia de los objetos, modificar un Map durante la iteración no afecta a los elementos que aún no se han visitado.
const dataMap = new Map([
  ["a", 1],
  ["b", 2],
  ["c", 3]
]);

// Modificar durante la iteración
for (const [key, value] of dataMap) {
  console.log(`Procesando ${key}:${value}`);
  
  // Agregar un nuevo elemento durante la iteración
  if (key === "b") {
    dataMap.set("d", 4);
  }
}
// Procesando a:1
// Procesando b:2
// Procesando c:3
// Procesando d:4 (se agrega y se procesa correctamente)

console.log([...dataMap.keys()]); // ["a", "b", "c", "d"]
  • Rendimiento con grandes conjuntos de datos: Para Map con muchos elementos, considerar métodos que eviten múltiples iteraciones completas.
const largeMap = new Map();
// Imagina que esto tiene miles de entradas
for (let i = 0; i < 10000; i++) {
  largeMap.set(`key${i}`, i);
}

// Ineficiente: múltiples iteraciones completas
const keysArray = [...largeMap.keys()];
const valuesArray = [...largeMap.values()];

// Más eficiente: una sola iteración
const keys = [];
const values = [];
for (const [k, v] of largeMap) {
  keys.push(k);
  values.push(v);
}

Casos de uso prácticos para la iteración de Maps

Agrupación y análisis de datos

const salesData = [
  { product: "Laptop", category: "Electronics", amount: 1200 },
  { product: "Headphones", category: "Electronics", amount: 100 },
  { product: "Book", category: "Media", amount: 25 },
  { product: "Smartphone", category: "Electronics", amount: 800 },
  { product: "Magazine", category: "Media", amount: 8 },
  { product: "Tablet", category: "Electronics", amount: 300 }
];

// Agrupar ventas por categoría
const salesByCategory = new Map();

for (const sale of salesData) {
  if (!salesByCategory.has(sale.category)) {
    salesByCategory.set(sale.category, []);
  }
  salesByCategory.get(sale.category).push(sale);
}

// Calcular total de ventas por categoría
const categoryTotals = new Map();

for (const [category, sales] of salesByCategory) {
  const total = sales.reduce((sum, sale) => sum + sale.amount, 0);
  categoryTotals.set(category, total);
}

console.log([...categoryTotals]); 
// [["Electronics", 2400], ["Media", 33]]

Implementación de un historial con capacidad de deshacer/rehacer

class EditHistory {
  constructor(initialState) {
    this.history = new Map();
    this.currentIndex = 0;
    this.maxIndex = 0;
    
    // Guardar estado inicial
    this.history.set(this.currentIndex, initialState);
  }

  addState(newState) {
    // Eliminar estados futuros si estamos en medio del historial
    for (let i = this.currentIndex + 1; i <= this.maxIndex; i++) {
      this.history.delete(i);
    }
    
    // Añadir nuevo estado
    this.currentIndex++;
    this.maxIndex = this.currentIndex;
    this.history.set(this.currentIndex, newState);
    
    return this.currentIndex;
  }

  undo() {
    if (this.currentIndex > 0) {
      this.currentIndex--;
      return this.history.get(this.currentIndex);
    }
    return null;
  }

  redo() {
    if (this.currentIndex < this.maxIndex) {
      this.currentIndex++;
      return this.history.get(this.currentIndex);
    }
    return null;
  }

  getCurrentState() {
    return this.history.get(this.currentIndex);
  }

  getHistory() {
    // Devolver array ordenado de estados
    return [...this.history.entries()]
      .sort(([indexA], [indexB]) => indexA - indexB)
      .map(([index, state]) => ({
        index,
        state,
        isCurrent: index === this.currentIndex
      }));
  }
}

// Uso
const textEditor = new EditHistory("Documento inicial");
textEditor.addState("Añadido primer párrafo");
textEditor.addState("Añadido segundo párrafo");
textEditor.addState("Corregida ortografía");

console.log(textEditor.getCurrentState()); // "Corregida ortografía"

const previousState = textEditor.undo();
console.log(previousState); // "Añadido segundo párrafo"

textEditor.addState("Añadido título");
console.log(textEditor.getHistory().map(h => h.state));
// ["Documento inicial", "Añadido primer párrafo", "Añadido segundo párrafo", "Añadido título"]

La capacidad de iterar eficientemente sobre un Map es una de sus características más poderosas y versátiles. Los diversos métodos de iteración, combinados con las características modernas de JavaScript como la desestructuración y los generadores, permiten implementar soluciones elegantes para una amplia gama de problemas de programación. Dominar estas técnicas de iteración es fundamental para aprovechar al máximo el potencial de la estructura de datos Map en aplicaciones JavaScript modernas.

Técnicas avanzadas y casos de uso específicos

El objeto Map en JavaScript va más allá de ser una simple estructura de datos clave-valor. Su diseño y características lo convierten en una herramienta extremadamente versátil para resolver problemas complejos y crear patrones de programación avanzados. En esta sección exploraremos técnicas sofisticadas y casos de uso específicos que aprovechan todo el potencial de esta estructura.

Mapas bidireccionales

Un mapa bidireccional permite buscar valores por clave y claves por valor, lo que resulta útil en escenarios donde necesitamos realizar búsquedas en ambas direcciones:

class BidirectionalMap {
  constructor(entries = []) {
    this.keyToValue = new Map();
    this.valueToKey = new Map();
    
    for (const [key, value] of entries) {
      this.set(key, value);
    }
  }

  set(key, value) {
    // Eliminar mapeos antiguos si existen
    if (this.keyToValue.has(key)) {
      const oldValue = this.keyToValue.get(key);
      this.valueToKey.delete(oldValue);
    }
    
    if (this.valueToKey.has(value)) {
      const oldKey = this.valueToKey.get(value);
      this.keyToValue.delete(oldKey);
    }
    
    // Establecer nuevos mapeos
    this.keyToValue.set(key, value);
    this.valueToKey.set(value, key);
    return this;
  }

  getByKey(key) {
    return this.keyToValue.get(key);
  }

  getByValue(value) {
    return this.valueToKey.get(value);
  }

  deleteByKey(key) {
    if (this.keyToValue.has(key)) {
      const value = this.keyToValue.get(key);
      this.keyToValue.delete(key);
      this.valueToKey.delete(value);
      return true;
    }
    return false;
  }

  deleteByValue(value) {
    if (this.valueToKey.has(value)) {
      const key = this.valueToKey.get(value);
      this.valueToKey.delete(value);
      this.keyToValue.delete(key);
      return true;
    }
    return false;
  }
}

// Ejemplo: Sistema de traducción
const translations = new BidirectionalMap([
  ["hello", "hola"],
  ["goodbye", "adiós"],
  ["thank you", "gracias"]
]);

console.log(translations.getByKey("hello")); // "hola"
console.log(translations.getByValue("gracias")); // "thank you"

Implementación de un sistema de caché con prioridad

Podemos crear una caché que no solo almacene datos, sino que también gestione su prioridad y tiempo de vida:

class PriorityCache {
  constructor(maxSize = 100) {
    this.cache = new Map();
    this.maxSize = maxSize;
    this.priorities = new Map();
  }

  set(key, value, priority = 0, ttl = null) {
    // Si la clave ya existe, actualizar su valor y prioridad
    if (this.cache.has(key)) {
      this.cache.set(key, {
        value,
        expiry: ttl ? Date.now() + ttl : null,
        lastAccessed: Date.now()
      });
      this.priorities.set(key, priority);
      return;
    }
    
    // Si alcanzamos el tamaño máximo, eliminar el elemento con menor prioridad
    if (this.cache.size >= this.maxSize) {
      this.evictLeastValuable();
    }
    
    // Añadir nuevo elemento
    this.cache.set(key, {
      value,
      expiry: ttl ? Date.now() + ttl : null,
      lastAccessed: Date.now()
    });
    this.priorities.set(key, priority);
  }

  get(key) {
    // Verificar si la clave existe
    if (!this.cache.has(key)) return undefined;
    
    const item = this.cache.get(key);
    
    // Verificar si ha expirado
    if (item.expiry && Date.now() > item.expiry) {
      this.cache.delete(key);
      this.priorities.delete(key);
      return undefined;
    }
    
    // Actualizar tiempo de último acceso
    item.lastAccessed = Date.now();
    this.cache.set(key, item);
    
    return item.value;
  }

  evictLeastValuable() {
    let leastValuableKey = null;
    let lowestValue = Infinity;
    
    // Calcular valor combinando prioridad y tiempo de acceso
    for (const [key, priority] of this.priorities) {
      const item = this.cache.get(key);
      // Si ha expirado, eliminarlo directamente
      if (item.expiry && Date.now() > item.expiry) {
        this.cache.delete(key);
        this.priorities.delete(key);
        continue;
      }
      
      // Valor = prioridad - (factor de antigüedad)
      const ageValue = (Date.now() - item.lastAccessed) / 10000;
      const value = priority - ageValue;
      
      if (value < lowestValue) {
        lowestValue = value;
        leastValuableKey = key;
      }
    }
    
    if (leastValuableKey) {
      this.cache.delete(leastValuableKey);
      this.priorities.delete(leastValuableKey);
    }
  }
}

// Uso: Caché de recursos con diferentes prioridades
const resourceCache = new PriorityCache(5);

// Recursos críticos con alta prioridad
resourceCache.set("config", { theme: "dark", fontSize: 14 }, 10);
resourceCache.set("userProfile", { name: "Alice", role: "admin" }, 8);

// Recursos menos importantes
resourceCache.set("recentPosts", ["Post 1", "Post 2"], 3, 60000); // 1 minuto TTL
resourceCache.set("sidebarData", { categories: ["Tech", "Science"] }, 2);
resourceCache.set("footerLinks", ["About", "Contact"], 1);

// Al añadir un nuevo elemento, se eliminará "footerLinks" (menor prioridad)
resourceCache.set("headerData", { logo: "logo.png" }, 4);

console.log(resourceCache.get("footerLinks")); // undefined (evicted)
console.log(resourceCache.get("userProfile")); // { name: "Alice", role: "admin" }

Implementación de un sistema de enrutamiento

Los objetos Map son ideales para implementar sistemas de enrutamiento en aplicaciones web:

class Router {
  constructor() {
    this.routes = new Map();
    this.paramRoutes = new Map();
  }

  add(path, handler) {
    // Comprobar si la ruta contiene parámetros
    if (path.includes(':')) {
      // Convertir la ruta a una expresión regular
      const pattern = path.replace(/:([^/]+)/g, '([^/]+)');
      const regex = new RegExp(`^${pattern}$`);
      
      // Extraer nombres de parámetros
      const paramNames = [];
      path.replace(/:([^/]+)/g, (_, paramName) => {
        paramNames.push(paramName);
      });
      
      this.paramRoutes.set(regex, { handler, paramNames });
    } else {
      this.routes.set(path, handler);
    }
  }

  navigate(path) {
    // Primero buscar rutas exactas
    if (this.routes.has(path)) {
      const handler = this.routes.get(path);
      return handler({});
    }
    
    // Luego buscar rutas con parámetros
    for (const [regex, { handler, paramNames }] of this.paramRoutes) {
      const match = path.match(regex);
      if (match) {
        // Extraer valores de parámetros
        const params = {};
        match.slice(1).forEach((value, index) => {
          params[paramNames[index]] = value;
        });
        
        return handler(params);
      }
    }
    
    // Ruta no encontrada
    return null;
  }
}

// Uso
const router = new Router();

router.add('/', () => {
  console.log('Página de inicio');
  return 'Home Page';
});

router.add('/users', () => {
  console.log('Lista de usuarios');
  return 'User List';
});

router.add('/users/:id', (params) => {
  console.log(`Perfil del usuario ${params.id}`);
  return `User Profile: ${params.id}`;
});

router.add('/posts/:category/:postId', (params) => {
  console.log(`Post ${params.postId} en categoría ${params.category}`);
  return `Post: ${params.postId}, Category: ${params.category}`;
});

console.log(router.navigate('/')); // "Home Page"
console.log(router.navigate('/users/42')); // "User Profile: 42"
console.log(router.navigate('/posts/technology/123')); // "Post: 123, Category: technology"

Implementación de un sistema de máquina de estados

Los Map son perfectos para implementar máquinas de estados finitos, útiles en muchos escenarios de programación:

class StateMachine {
  constructor(initialState) {
    this.currentState = initialState;
    this.states = new Map();
    this.transitions = new Map();
    this.globalHandlers = new Map();
  }

  addState(name, handlers = {}) {
    this.states.set(name, handlers);
    return this;
  }

  addTransition(fromState, event, toState, action = null) {
    const key = `${fromState}:${event}`;
    this.transitions.set(key, { toState, action });
    return this;
  }

  addGlobalHandler(event, handler) {
    this.globalHandlers.set(event, handler);
    return this;
  }

  trigger(event, data = {}) {
    console.log(`Event: ${event} in state: ${this.currentState}`);
    
    // Verificar si hay un manejador global para este evento
    if (this.globalHandlers.has(event)) {
      this.globalHandlers.get(event)(data, this.currentState);
    }
    
    // Verificar si el estado actual tiene un manejador para este evento
    const stateHandlers = this.states.get(this.currentState);
    if (stateHandlers && typeof stateHandlers[event] === 'function') {
      stateHandlers[event](data);
    }
    
    // Verificar si hay una transición definida
    const transitionKey = `${this.currentState}:${event}`;
    if (this.transitions.has(transitionKey)) {
      const { toState, action } = this.transitions.get(transitionKey);
      
      // Ejecutar acción de transición si existe
      if (action) {
        action(data, this.currentState, toState);
      }
      
      // Cambiar al nuevo estado
      const oldState = this.currentState;
      this.currentState = toState;
      
      // Ejecutar manejadores de entrada/salida de estado
      const oldStateHandlers = this.states.get(oldState);
      if (oldStateHandlers && typeof oldStateHandlers.onExit === 'function') {
        oldStateHandlers.onExit(toState);
      }
      
      const newStateHandlers = this.states.get(toState);
      if (newStateHandlers && typeof newStateHandlers.onEnter === 'function') {
        newStateHandlers.onEnter(oldState);
      }
      
      console.log(`Transition: ${oldState} -> ${toState}`);
    }
    
    return this.currentState;
  }
}

// Ejemplo: Máquina de estados para un proceso de pedido
const orderProcess = new StateMachine('cart');

// Definir estados
orderProcess
  .addState('cart', {
    onEnter: () => console.log('Carrito abierto'),
    onExit: () => console.log('Finalizando carrito')
  })
  .addState('checkout', {
    onEnter: () => console.log('Iniciando checkout'),
    validatePayment: (data) => console.log(`Validando pago: ${data.amount}`)
  })
  .addState('processing', {
    onEnter: () => console.log('Procesando pedido')
  })
  .addState('completed', {
    onEnter: () => console.log('Pedido completado')
  })
  .addState('cancelled', {
    onEnter: (prevState) => console.log(`Pedido cancelado desde ${prevState}`)
  });

// Definir transiciones
orderProcess
  .addTransition('cart', 'checkout', 'checkout')
  .addTransition('checkout', 'submit', 'processing', 
    (data) => console.log(`Enviando datos de pago: ${data.method}`))
  .addTransition('processing', 'complete', 'completed')
  .addTransition('cart', 'cancel', 'cancelled')
  .addTransition('checkout', 'cancel', 'cancelled')
  .addTransition('processing', 'fail', 'cancelled');

// Manejador global
orderProcess.addGlobalHandler('log', 
  (data, state) => console.log(`LOG [${state}]: ${data.message}`));

// Simular flujo de pedido
orderProcess.trigger('checkout'); // cart -> checkout
orderProcess.trigger('log', { message: 'Usuario verificado' });
orderProcess.trigger('validatePayment', { amount: 99.99 });
orderProcess.trigger('submit', { method: 'credit_card' }); // checkout -> processing
orderProcess.trigger('complete'); // processing -> completed

Implementación de un sistema de observables reactivos

Podemos usar Map para crear un sistema de observables que notifique a los suscriptores cuando los datos cambian:

class Observable {
  constructor(initialValue) {
    this._value = initialValue;
    this.subscribers = new Map();
    this.nextId = 1;
  }

  get value() {
    return this._value;
  }

  set value(newValue) {
    const oldValue = this._value;
    this._value = newValue;
    
    // Notificar a todos los suscriptores
    for (const callback of this.subscribers.values()) {
      callback(newValue, oldValue);
    }
  }

  subscribe(callback) {
    const id = this.nextId++;
    this.subscribers.set(id, callback);
    
    // Ejecutar inmediatamente con el valor actual
    callback(this._value, undefined);
    
    // Devolver función para cancelar suscripción
    return () => {
      this.subscribers.delete(id);
    };
  }

  map(transformFn) {
    const derived = new Observable(transformFn(this._value));
    
    // Cuando este observable cambie, actualizar el derivado
    this.subscribe((newValue) => {
      derived.value = transformFn(newValue);
    });
    
    return derived;
  }
}

// Uso: Sistema reactivo simple
const counter = new Observable(0);
const doubled = counter.map(value => value * 2);
const isEven = counter.map(value => value % 2 === 0);

// Suscribirse a cambios
const unsubCounter = counter.subscribe(
  value => console.log(`Counter: ${value}`)
);

doubled.subscribe(
  value => console.log(`Doubled: ${value}`)
);

isEven.subscribe(
  value => console.log(`Is even: ${value}`)
);

// Cambiar el valor desencadena todas las notificaciones
counter.value = 1;
// Counter: 1
// Doubled: 2
// Is even: false

counter.value = 2;
// Counter: 2
// Doubled: 4
// Is even: true

// Cancelar suscripción
unsubCounter();

counter.value = 3;
// (No hay log de "Counter")
// Doubled: 6
// Is even: false

Implementación de un sistema de validación de formularios

Los Map son útiles para crear sistemas de validación flexibles:

class FormValidator {
  constructor() {
    this.validators = new Map();
    this.errors = new Map();
  }

  addField(fieldName, validators) {
    this.validators.set(fieldName, validators);
    return this;
  }

  validate(formData) {
    this.errors.clear();
    let isValid = true;
    
    for (const [fieldName, fieldValidators] of this.validators) {
      // Obtener valor del campo
      const value = formData[fieldName];
      const fieldErrors = [];
      
      // Aplicar cada validador
      for (const { rule, message } of fieldValidators) {
        if (!rule(value, formData)) {
          fieldErrors.push(message);
          isValid = false;
        }
      }
      
      // Almacenar errores si los hay
      if (fieldErrors.length > 0) {
        this.errors.set(fieldName, fieldErrors);
      }
    }
    
    return isValid;
  }

  getErrors(fieldName = null) {
    if (fieldName) {
      return this.errors.get(fieldName) || [];
    }
    
    // Devolver todos los errores
    const allErrors = {};
    for (const [field, errors] of this.errors) {
      allErrors[field] = errors;
    }
    return allErrors;
  }
}

// Reglas de validación comunes
const rules = {
  required: value => value !== undefined && value !== null && value !== '',
  minLength: min => value => value.length >= min,
  maxLength: max => value => value.length <= max,
  email: value => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
  matches: fieldName => (value, formData) => value === formData[fieldName],
  numeric: value => /^\d+$/.test(value)
};

// Uso
const validator = new FormValidator();

validator
  .addField('username', [
    { rule: rules.required, message: 'El nombre de usuario es obligatorio' },
    { rule: rules.minLength(3), message: 'El nombre debe tener al menos 3 caracteres' }
  ])
  .addField('email', [
    { rule: rules.required, message: 'El email es obligatorio' },
    { rule: rules.email, message: 'El formato de email no es válido' }
  ])
  .addField('password', [
    { rule: rules.required, message: 'La contraseña es obligatoria' },
    { rule: rules.minLength(8), message: 'La contraseña debe tener al menos 8 caracteres' }
  ])
  .addField('confirmPassword', [
    { rule: rules.required, message: 'Debe confirmar la contraseña' },
    { rule: rules.matches('password'), message: 'Las contraseñas no coinciden' }
  ]);

// Validar datos de formulario
const formData = {
  username: 'jo',
  email: 'invalid-email',
  password: 'pass123',
  confirmPassword: 'pass456'
};

const isValid = validator.validate(formData);
console.log('Formulario válido:', isValid); // false
console.log('Errores:', validator.getErrors());
// {
//   username: ['El nombre debe tener al menos 3 caracteres'],
//   email: ['El formato de email no es válido'],
//   password: ['La contraseña debe tener al menos 8 caracteres'],
//   confirmPassword: ['Las contraseñas no coinciden']
// }

Implementación de un sistema de middleware

Los Map pueden utilizarse para crear un sistema de middleware similar al de frameworks como Express:

class MiddlewareChain {
  constructor() {
    this.middlewares = new Map();
    this.globalMiddlewares = [];
    this.nextId = 1;
  }

  use(path, handler) {
    if (typeof path === 'function') {
      // Middleware global
      this.globalMiddlewares.push(path);
    } else {
      // Middleware específico de ruta
      if (!this.middlewares.has(path)) {
        this.middlewares.set(path, []);
      }
      this.middlewares.get(path).push(handler);
    }
    return this;
  }

  async process(path, context = {}) {
    const middlewares = [
      ...this.globalMiddlewares,
      ...(this.middlewares.get(path) || [])
    ];
    
    let index = 0;
    
    const next = async () => {
      if (index >= middlewares.length) return;
      
      const currentMiddleware = middlewares[index++];
      await currentMiddleware(context, next);
    };
    
    await next();
    return context;
  }
}

// Uso: Pipeline de procesamiento de imágenes
const imageProcessor = new MiddlewareChain();

// Middleware global - logging
imageProcessor.use(async (ctx, next) => {
  console.log(`Procesando imagen: ${ctx.filename}`);
  const startTime = Date.now();
  
  await next();
  
  const duration = Date.now() - startTime;
  console.log(`Procesamiento completado en ${duration}ms`);
});

// Middleware específico para redimensionar
imageProcessor.use('/resize', async (ctx, next) => {
  console.log(`Redimensionando a ${ctx.width}x${ctx.height}`);
  // Simulamos procesamiento
  await new Promise(resolve => setTimeout(resolve, 200));
  ctx.size = `${ctx.width}x${ctx.height}`;
  
  await next();
});

// Middleware para aplicar filtros
imageProcessor.use('/filter', async (ctx, next) => {
  console.log(`Aplicando filtro: ${ctx.filter}`);
  // Simulamos procesamiento
  await new Promise(resolve => setTimeout(resolve, 150));
  ctx.filtered = true;
  
  await next();
});

// Middleware para comprimir
imageProcessor.use('/compress', async (ctx, next) => {
  console.log(`Comprimiendo con calidad: ${ctx.quality}`);
  // Simulamos procesamiento
  await new Promise(resolve => setTimeout(resolve, 100));
  ctx.compressed = true;
  ctx.filesize = `${Math.floor(Math.random() * 500)}KB`;
  
  await next();
});

// Procesar una imagen
async function processImage() {
  const result = await imageProcessor.process('/resize', {
    filename: 'vacation.jpg',
    width: 800,
    height: 600
  });
  
  console.log('Resultado:', result);
  
  const result2 = await imageProcessor.process('/filter', {
    filename: 'profile.png',
    filter: 'sepia'
  });
  
  console.log('Resultado 2:', result2);
}

processImage();

Implementación de un sistema de plugins

Los Map son ideales para crear sistemas de plugins extensibles:

class PluginSystem {
  constructor() {
    this.plugins = new Map();
    this.hooks = new Map();
  }

  register(name, plugin) {
    if (this.plugins.has(name)) {
      console.warn(`Plugin '${name}' ya está registrado. Sobrescribiendo.`);
    }
    
    this.plugins.set(name, plugin);
    
    // Registrar hooks del plugin
    if (plugin.hooks) {
      for (const [hookName, handler] of Object.entries(plugin.hooks)) {
        this.addHookHandler(hookName, name, handler);
      }
    }
    
    // Inicializar plugin si tiene método init
    if (typeof plugin.init === 'function') {
      plugin.init(this);
    }
    
    return this;
  }

  addHookHandler(hookName, pluginName, handler) {
    if (!this.hooks.has(hookName)) {
      this.hooks.set(hookName, new Map());
    }
    
    this.hooks.get(hookName).set(pluginName, handler);
  }

  async executeHook(hookName, context = {}) {
    if (!this.hooks.has(hookName)) {
      return context;
    }
    
    const handlers = this.hooks.get(hookName);
    const results = [];
    
    for (const [pluginName, handler] of handlers) {
      try {
        const result = await handler(context);
        results.push({ pluginName, result });
      } catch (error) {
        console.error(`Error en plugin '${pluginName}' para hook '${hookName}':`, error);
      }
    }
    
    return { context, results };
  }

  getPlugin(name) {
    return this.plugins.get(name);
  }

  listPlugins() {
    return Array.from(this.plugins.keys());
  }
}

// Ejemplo: Sistema de plugins para un editor de texto
const editor = new PluginSystem();

// Plugin de autoguardado
editor.register('autosave', {
  init(system) {
    console.log('Plugin de autoguardado inicializado');
    // Configurar temporizador de autoguardado
    setInterval(() => {
      system.executeHook('save', { timestamp: Date.now() });
    }, 5000);
  },
  hooks: {
    save: async (context) => {
      console.log(`Autoguardando documento en ${context.timestamp}`);
      return { success: true, method: 'autosave' };
    }
  }
});

// Plugin de corrección ortográfica
editor.register('spellcheck', {
  hooks: {
    textChanged: async (context) => {
      console.log(`Verificando ortografía: "${context.text}"`);
      // Simulamos encontrar errores
      const errors = context.text.split(' ')
        .filter(word => word.length < 3)
        .map(word => ({ word, suggestion: word + 'corrected' }));
      
      return { errors };
    }
  }
});

// Plugin de estadísticas
editor.register('stats', {
  hooks: {
    textChanged: async (context) => {
      const words = context.text.split(/\s+/).filter(w => w.length > 0);
      return {
        wordCount: words.length,
        charCount: context.text.length
      };
    },
    save: async (context) => {
      console.log(`Guardando estadísticas en ${context.timestamp}`);
      return { success: true, method: 'stats' };
    }
  }
});

// Simular cambio de texto
async function simulateTextChange() {
  const result = await editor.executeHook('textChanged', {
    text: 'Un ejemplo de texto con un par de errores'
  });
  
  console.log('Resultados de plugins:', result);
}

simulateTextChange();
console.log('Plugins instalados:', editor.listPlugins());

Implementación de un sistema de inyección de dependencias

Los Map son perfectos para crear un contenedor de inyección de dependencias simple:

class DependencyContainer {
  constructor() {
    this.singletons = new Map();
    this.factories = new Map();
    this.instances = new Map();
  }

  register(name, factory, singleton = false) {
    if (singleton) {
      this.singletons.set(name, factory);
    } else {
      this.factories.set(name, factory);
    }
    return this;
  }

  get(name) {
    // Verificar si ya tenemos una instancia singleton
    if (this.instances.has(name)) {
      return this.instances.get(name);
    }
    
    // Verificar si es un singleton
    if (this.singletons.has(name)) {
      const factory = this.singletons.get(name);
      const instance = typeof factory === 'function' 
        ? factory(this) 
        : factory;
      
      // Guardar instancia para futuros usos
      this.instances.set(name, instance);
      return instance;
    }
    
    // Verificar si es una factory
    if (this.factories.has(name)) {
      const factory = this.factories.get(name);
      return typeof factory === 'function' 
        ? factory(this) 
        : factory;
    }
    
    throw new Error(`Dependencia no registrada: ${name}`);
  }

  has(name) {
    return this.singletons.has(name) || 
           this.factories.has(name) || 
           this.instances.has(name);
  }
}

// Uso: Sistema de inyección de dependencias
const container = new DependencyContainer();

// Registrar servicios
container
  .register('config', {
    apiUrl: 'https://api.example.com',
    timeout: 5000
  }, true) // singleton
  
  .register('logger', (container) => ({
    log: (message) => console.log(`[LOG] ${message}`),
    error: (message) => console.error(`[ERROR] ${message}`)
  }), true) // singleton
  
  .register('apiClient', (container) => {
    const config = container.get('config');
    const logger = container.get('logger');
    
    return {
      fetch: async (endpoint) => {
        logger.log(`Fetching ${endpoint} from ${config.apiUrl}`);
        // Simulación de fetch
        await new Promise(resolve => setTimeout(resolve, 100));
        return { data: `Response from ${endpoint}` };
      }
    };
  });

// Usar servicios
const apiClient = container.get('apiClient');
apiClient.fetch('/users').then(response => {
  console.log(response);
});

// Los singletons devuelven la misma instancia
const logger1 = container.get('logger');
const logger2 = container.get('logger');
console.log('Misma instancia de logger:', logger1 === logger2); // true

// Las factories devuelven nuevas instancias
const api1 = container.get('apiClient');
const api2 = container.get('apiClient');
console.log('Diferentes instancias de apiClient:', api1 !== api2); // true

El objeto Map en JavaScript es una estructura de datos increíblemente versátil que va mucho más allá de su propósito básico de almacenar pares clave-valor. Como hemos visto, puede utilizarse para implementar patrones de diseño complejos, sistemas de gestión de estado, caches avanzadas, enrutadores, sistemas de plugins y mucho más.

La combinación de su capacidad para usar cualquier tipo de dato como clave, su preservación del orden de inserción, y su API intuitiva lo convierten en una herramienta fundamental para resolver problemas complejos en aplicaciones JavaScript modernas. Dominar estas técnicas avanzadas permite a los desarrolladores crear soluciones elegantes, mantenibles y eficientes para una amplia gama de escenarios.

CONSTRUYE TU CARRERA EN IA Y PROGRAMACIÓN SOFTWARE

Accede a +1000 lecciones y cursos con certificado. Mejora tu portfolio con certificados de superación para tu CV.

30 % DE DESCUENTO

Plan mensual

19.00 /mes

13.30 € /mes

Precio normal mensual: 19 €
63 % DE DESCUENTO

Plan anual

10.00 /mes

7.00 € /mes

Ahorras 144 € al año
Precio normal anual: 120 €
Aprende JavaScript online

Ejercicios de esta lección Mapas con Map

Evalúa tus conocimientos de esta lección Mapas con Map con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.

Funciones flecha

JavaScript
Puzzle

Polimorfismo

JavaScript
Test

Array

JavaScript
Código

Transformación con map()

JavaScript
Test

Gestor de tareas con JavaScript

JavaScript
Proyecto

Manipulación DOM

JavaScript
Test

Funciones

JavaScript
Test

Funciones flecha

JavaScript
Código

Async / Await

JavaScript
Código

Creación y uso de variables

JavaScript
Test

Excepciones

JavaScript
Puzzle

Promises

JavaScript
Código

Funciones cierre (closure)

JavaScript
Test

Herencia

JavaScript
Puzzle

Herencia

JavaScript
Test

Estructuras de control

JavaScript
Código

Selección de elementos DOM

JavaScript
Test

Modificación de elementos DOM

JavaScript
Test

Filtrado con filter() y find()

JavaScript
Test

Funciones cierre (closure)

JavaScript
Puzzle

Funciones

JavaScript
Puzzle

Mapas con Map

JavaScript
Test

Reducción con reduce()

JavaScript
Test

Callbacks

JavaScript
Puzzle

Manipulación DOM

JavaScript
Puzzle

Promises

JavaScript
Test

Async / Await

JavaScript
Test

Eventos del DOM

JavaScript
Puzzle

Async / Await

JavaScript
Puzzle

Promises

JavaScript
Puzzle

Filtrado con filter() y find()

JavaScript
Código

Callbacks

JavaScript
Test

Creación de clases y objetos Restaurante

JavaScript
Código

Reducción con reduce()

JavaScript
Código

Filtrado con filter() y find()

JavaScript
Puzzle

Reducción con reduce()

JavaScript
Puzzle

Conjuntos con Set

JavaScript
Puzzle

Herencia de clases

JavaScript
Código

Eventos del DOM

JavaScript
Test

Clases y objetos

JavaScript
Puzzle

Modificación de elementos DOM

JavaScript
Puzzle

Mapas con Map

JavaScript
Puzzle

Introducción a JavaScript

JavaScript
Test

Funciones

JavaScript
Código

Tipos de datos

JavaScript
Test

Clases y objetos

JavaScript
Test

Array

JavaScript
Test

Conjuntos con Set

JavaScript
Test

Array

JavaScript
Puzzle

Encapsulación

JavaScript
Puzzle

Clases y objetos

JavaScript
Código

Uso de operadores

JavaScript
Puzzle

Uso de operadores

JavaScript
Test

Estructuras de control

JavaScript
Test

Excepciones

JavaScript
Test

Transformación con map()

JavaScript
Puzzle

Funciones flecha

JavaScript
Test

Selección de elementos DOM

JavaScript
Puzzle

Encapsulación

JavaScript
Test

Mapas con Map

JavaScript
Código

Creación y uso de variables

JavaScript
Puzzle

Polimorfismo

JavaScript
Puzzle

Tipos de datos

JavaScript
Puzzle

Estructuras de control

JavaScript
Puzzle

Todas las lecciones de JavaScript

Accede a todas las lecciones de JavaScript y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.

Accede GRATIS a JavaScript y certifícate

Certificados de superación de JavaScript

Supera todos los ejercicios de programación del curso de JavaScript y obtén certificados de superación para mejorar tu currículum y tu empleabilidad.

En esta lección

Objetivos de aprendizaje de esta lección

  1. Entender qué es un Map en JavaScript y cómo se diferencia de otros objetos como Arrays u objetos literales.
  2. Conocer las diferentes formas de crear un Map utilizando el constructor Map y cómo inicializarlo con pares clave-valor.
  3. Familiarizarse con las propiedades y métodos disponibles para trabajar con Maps, como size, set(key, value), get(key), has(key), delete(key), y clear().
  4. Aprender cómo iterar sobre los elementos de un Map utilizando los métodos keys(), values(), entries(), y forEach(callback[, thisArg]), o mediante un bucle for...of.
  5. Comprender las ventajas de utilizar Maps, como la capacidad de almacenar cualquier tipo de valor como clave, el mantenimiento del orden de inserción y la eficiencia en la manipulación de datos.