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ícateIntroducció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.
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
Polimorfismo
Array
Transformación con map()
Gestor de tareas con JavaScript
Manipulación DOM
Funciones
Funciones flecha
Async / Await
Creación y uso de variables
Excepciones
Promises
Funciones cierre (closure)
Herencia
Herencia
Estructuras de control
Selección de elementos DOM
Modificación de elementos DOM
Filtrado con filter() y find()
Funciones cierre (closure)
Funciones
Mapas con Map
Reducción con reduce()
Callbacks
Manipulación DOM
Promises
Async / Await
Eventos del DOM
Async / Await
Promises
Filtrado con filter() y find()
Callbacks
Creación de clases y objetos Restaurante
Reducción con reduce()
Filtrado con filter() y find()
Reducción con reduce()
Conjuntos con Set
Herencia de clases
Eventos del DOM
Clases y objetos
Modificación de elementos DOM
Mapas con Map
Introducción a JavaScript
Funciones
Tipos de datos
Clases y objetos
Array
Conjuntos con Set
Array
Encapsulación
Clases y objetos
Uso de operadores
Uso de operadores
Estructuras de control
Excepciones
Transformación con map()
Funciones flecha
Selección de elementos DOM
Encapsulación
Mapas con Map
Creación y uso de variables
Polimorfismo
Tipos de datos
Estructuras de control
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.
Introducción A Javascript
Introducción Y Entorno
Tipos De Datos
Sintaxis
Variables
Sintaxis
Operadores
Sintaxis
Estructuras De Control
Sintaxis
Funciones
Sintaxis
Funciones Cierre (Closure)
Sintaxis
Arrays Y Métodos
Estructuras De Datos
Conjuntos Con Set
Estructuras De Datos
Mapas Con Map
Estructuras De Datos
Funciones Flecha
Programación Funcional
Filtrado Con Filter() Y Find()
Programación Funcional
Transformación Con Map()
Programación Funcional
Reducción Con Reduce()
Programación Funcional
Clases Y Objetos
Programación Orientada A Objetos
Excepciones
Programación Orientada A Objetos
Encapsulación
Programación Orientada A Objetos
Herencia
Programación Orientada A Objetos
Polimorfismo
Programación Orientada A Objetos
Manipulación Dom
Dom
Selección De Elementos Dom
Dom
Modificación De Elementos Dom
Dom
Eventos Del Dom
Dom
Callbacks
Programación Asíncrona
Promises
Programación Asíncrona
Async / Await
Programación Asíncrona
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
- Entender qué es un
Map
en JavaScript y cómo se diferencia de otros objetos como Arrays u objetos literales. - Conocer las diferentes formas de crear un
Map
utilizando el constructorMap
y cómo inicializarlo con pares clave-valor. - Familiarizarse con las propiedades y métodos disponibles para trabajar con
Maps
, comosize
,set(key, value)
,get(key)
,has(key)
,delete(key)
, yclear()
. - Aprender cómo iterar sobre los elementos de un
Map
utilizando los métodoskeys()
,values()
,entries()
, yforEach(callback[, thisArg])
, o mediante un buclefor...of
. - 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.