Introduction #
Imagine you’re building a library management system where each book needs a unique identifier. You need to track which ID corresponds to which book, manage checkout status, and quickly look up information. Traditional JavaScript objects work, but what if you need to use numbers, objects, or even functions as keys? This is where JavaScript’s Map data structure becomes invaluable.
Maps represent one of the most powerful additions to modern JavaScript, introduced in ES6 (ECMAScript 2015). They provide a robust, performant solution for managing key-value pairs with unprecedented flexibility. Unlike plain objects, Maps maintain insertion order, support any data type as keys, and offer optimized performance for frequent additions and deletions.
In this comprehensive guide, we’ll explore everything you need to know about JavaScript Maps - from fundamental concepts to advanced patterns, real-world applications, and performance optimization strategies.
What Are JavaScript Maps? #
A Map is a built-in JavaScript data structure that stores key-value pairs where both keys and values can be of any type. Think of it as an enhanced dictionary or hash table that overcomes many limitations of traditional JavaScript objects.
Core Characteristics #
Flexible Key Types: Unlike objects that coerce keys to strings, Maps accept any data type as keys - primitives, objects, functions, or even other Maps.
Insertion Order Preservation: Maps remember the original insertion order of keys, making them predictable when iterating.
Size Property: Maps provide a size
property that instantly returns the number of key-value pairs, unlike objects which require Object.keys()
iteration.
Optimized Performance: Maps are specifically designed for frequent additions and deletions, offering better performance than objects in these scenarios.
No Prototype Pollution: Maps don’t have default keys from prototypes, eliminating potential conflicts with property names like toString
or constructor
.
Creating and Initializing Maps #
Let’s explore the different ways to create and populate Maps.
Creating an Empty Map #
The simplest approach is instantiating an empty Map:
// Create an empty Map
const userSessions = new Map();
// Check the initial size
console.log(userSessions.size); // Output: 0
Initializing with Data #
You can populate a Map during creation by passing an iterable of key-value pairs:
// Initialize with an array of [key, value] pairs
const bookCatalog = new Map([
['ISBN-001', { title: 'The Great Gatsby', author: 'F. Scott Fitzgerald', year: 1925 }],
['ISBN-002', { title: 'To Kill a Mockingbird', author: 'Harper Lee', year: 1960 }],
['ISBN-003', { title: '1984', author: 'George Orwell', year: 1949 }],
['ISBN-004', { title: 'Pride and Prejudice', author: 'Jane Austen', year: 1813 }]
]);
console.log(bookCatalog.size); // Output: 4
console.log(bookCatalog.get('ISBN-001'));
// Output: { title: 'The Great Gatsby', author: 'F. Scott Fitzgerald', year: 1925 }
Converting Objects to Maps #
You can easily convert existing objects into Maps:
// Convert an object to a Map
const configObject = {
apiUrl: 'https://api.example.com',
timeout: 5000,
retryAttempts: 3
};
const configMap = new Map(Object.entries(configObject));
console.log(configMap.get('timeout')); // Output: 5000
Creating Maps from Other Iterables #
Maps can be created from any iterable that produces key-value pairs:
// Create a Map from entries of another Map
const originalMap = new Map([['a', 1], ['b', 2]]);
const clonedMap = new Map(originalMap);
// Create a Map from a generator function
function* idGenerator() {
for (let i = 1; i <= 3; i++) {
yield [`id-${i}`, { value: i * 10 }];
}
}
const generatedMap = new Map(idGenerator());
console.log(generatedMap.get('id-2')); // Output: { value: 20 }
The Power of Flexible Keys #
One of Map’s most distinctive features is its ability to use any value as a key. This capability opens up sophisticated programming patterns impossible with regular objects.
Using Primitive Types as Keys #
const statsMap = new Map();
// Numbers as keys
statsMap.set(1, 'First place');
statsMap.set(42, 'The answer to everything');
statsMap.set(3.14159, 'Approximation of Pi');
// Booleans as keys
statsMap.set(true, 'Success case');
statsMap.set(false, 'Failure case');
// Symbols as keys
const privateKey = Symbol('private');
statsMap.set(privateKey, 'Hidden data');
console.log(statsMap.get(42)); // Output: 'The answer to everything'
console.log(statsMap.get(true)); // Output: 'Success case'
Using Objects as Keys #
This is where Maps truly shine. You can use objects themselves as keys, which is perfect for associating metadata with objects:
// Use DOM elements as keys
const elementMetadata = new Map();
const button1 = document.createElement('button');
const button2 = document.createElement('button');
elementMetadata.set(button1, {
clickCount: 0,
lastClicked: null,
handler: 'submitForm'
});
elementMetadata.set(button2, {
clickCount: 0,
lastClicked: null,
handler: 'cancelForm'
});
// Retrieve metadata for a specific button
const button1Data = elementMetadata.get(button1);
console.log(button1Data); // Output: { clickCount: 0, lastClicked: null, handler: 'submitForm' }
Using Functions as Keys #
You can even use functions as keys for storing metadata about functions:
const functionRegistry = new Map();
function calculateTax(amount) {
return amount * 0.15;
}
function calculateDiscount(amount) {
return amount * 0.10;
}
// Store metadata about functions
functionRegistry.set(calculateTax, {
name: 'Tax Calculator',
category: 'Financial',
executionCount: 0
});
functionRegistry.set(calculateDiscount, {
name: 'Discount Calculator',
category: 'Pricing',
executionCount: 0
});
// Access function metadata
const taxInfo = functionRegistry.get(calculateTax);
console.log(taxInfo.name); // Output: 'Tax Calculator'
Key Equality and Identity #
Maps use the SameValueZero algorithm for key comparison, which means:
const identityMap = new Map();
// Numbers are compared by value
identityMap.set(1, 'one');
console.log(identityMap.get(1)); // Output: 'one'
// NaN is treated as equal to NaN (unlike ===)
identityMap.set(NaN, 'not a number');
console.log(identityMap.get(NaN)); // Output: 'not a number'
// Objects are compared by reference, not by content
const obj1 = { id: 1 };
const obj2 = { id: 1 }; // Different object with same content
identityMap.set(obj1, 'First object');
identityMap.set(obj2, 'Second object');
console.log(identityMap.size); // Output: 2 (both objects stored separately)
console.log(identityMap.get(obj1)); // Output: 'First object'
console.log(identityMap.get(obj2)); // Output: 'Second object'
Essential Map Operations #
Let’s explore the core methods that make Maps so powerful through a practical user authentication system example.
Setting and Updating Values #
The set()
method adds or updates key-value pairs and returns the Map itself, enabling method chaining:
class AuthenticationManager {
constructor() {
this.sessions = new Map();
this.loginAttempts = new Map();
}
createSession(userId, sessionData) {
// set() adds or updates a key-value pair
this.sessions.set(userId, {
token: sessionData.token,
loginTime: new Date(),
ipAddress: sessionData.ipAddress,
userAgent: sessionData.userAgent,
expiresAt: new Date(Date.now() + 3600000) // 1 hour
});
// Method chaining is possible
this.loginAttempts
.set(userId, { count: 0, lastAttempt: new Date() });
return this.sessions.get(userId);
}
updateSessionActivity(userId) {
const session = this.sessions.get(userId);
if (session) {
session.lastActivity = new Date();
// Re-setting updates the value
this.sessions.set(userId, session);
}
}
}
const authManager = new AuthenticationManager();
authManager.createSession('user_12345', {
token: 'abc123xyz',
ipAddress: '192.168.1.1',
userAgent: 'Mozilla/5.0...'
});
Retrieving Values #
The get()
method retrieves values by key, returning undefined
if the key doesn’t exist:
class UserPreferences {
constructor() {
this.preferences = new Map();
}
setPreference(userId, key, value) {
if (!this.preferences.has(userId)) {
this.preferences.set(userId, new Map());
}
const userPrefs = this.preferences.get(userId);
userPrefs.set(key, value);
}
getPreference(userId, key, defaultValue = null) {
const userPrefs = this.preferences.get(userId);
// Return default if user or preference doesn't exist
if (!userPrefs) return defaultValue;
return userPrefs.has(key) ? userPrefs.get(key) : defaultValue;
}
getAllPreferences(userId) {
return this.preferences.get(userId) || new Map();
}
}
const prefs = new UserPreferences();
prefs.setPreference('user_001', 'theme', 'dark');
prefs.setPreference('user_001', 'language', 'en');
prefs.setPreference('user_001', 'notifications', true);
console.log(prefs.getPreference('user_001', 'theme')); // Output: 'dark'
console.log(prefs.getPreference('user_001', 'fontSize', 14)); // Output: 14 (default)
console.log(prefs.getPreference('user_999', 'theme')); // Output: null
Checking Key Existence #
The has()
method checks if a key exists in the Map:
class FeatureFlags {
constructor() {
this.flags = new Map([
['newDashboard', { enabled: true, rolloutPercentage: 100 }],
['betaFeatures', { enabled: true, rolloutPercentage: 50 }],
['experimentalUI', { enabled: false, rolloutPercentage: 0 }]
]);
}
isFeatureEnabled(featureName, userId = null) {
// Check if feature exists
if (!this.flags.has(featureName)) {
console.warn(`Feature '${featureName}' not found`);
return false;
}
const feature = this.flags.get(featureName);
if (!feature.enabled) {
return false;
}
// Implement percentage rollout if userId provided
if (userId && feature.rolloutPercentage < 100) {
const hash = this.hashUserId(userId);
return hash % 100 < feature.rolloutPercentage;
}
return true;
}
hashUserId(userId) {
// Simple hash function for demonstration
return userId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0);
}
}
const features = new FeatureFlags();
console.log(features.isFeatureEnabled('newDashboard')); // Output: true
console.log(features.isFeatureEnabled('experimentalUI')); // Output: false
console.log(features.isFeatureEnabled('nonExistent')); // Output: false (with warning)
Deleting Entries #
The delete()
method removes a key-value pair and returns true
if the key existed:
class CacheManager {
constructor(maxSize = 100, ttl = 300000) { // 5 minutes default TTL
this.cache = new Map();
this.maxSize = maxSize;
this.ttl = ttl;
this.accessOrder = new Map(); // Track access for LRU
}
set(key, value) {
// Remove oldest entry if cache is full
if (this.cache.size >= this.maxSize && !this.cache.has(key)) {
this.evictOldest();
}
this.cache.set(key, {
value,
timestamp: Date.now()
});
this.accessOrder.set(key, Date.now());
}
get(key) {
const entry = this.cache.get(key);
if (!entry) return null;
// Check if entry has expired
if (Date.now() - entry.timestamp > this.ttl) {
this.delete(key);
return null;
}
// Update access time
this.accessOrder.set(key, Date.now());
return entry.value;
}
delete(key) {
this.accessOrder.delete(key);
return this.cache.delete(key); // Returns true if deleted
}
evictOldest() {
// Find the least recently accessed key
let oldestKey = null;
let oldestTime = Infinity;
for (const [key, time] of this.accessOrder) {
if (time < oldestTime) {
oldestTime = time;
oldestKey = key;
}
}
if (oldestKey) {
this.delete(oldestKey);
}
}
clear() {
this.cache.clear();
this.accessOrder.clear();
}
get size() {
return this.cache.size;
}
}
const cache = new CacheManager(3, 10000); // Max 3 items, 10 second TTL
cache.set('user:1', { name: 'Alice' });
cache.set('user:2', { name: 'Bob' });
cache.set('user:3', { name: 'Charlie' });
console.log(cache.size); // Output: 3
cache.delete('user:2');
console.log(cache.size); // Output: 2
Clearing All Entries #
The clear()
method removes all key-value pairs from the Map:
class SessionStore {
constructor() {
this.activeSessions = new Map();
}
addSession(sessionId, data) {
this.activeSessions.set(sessionId, {
...data,
createdAt: new Date()
});
}
clearAllSessions() {
const count = this.activeSessions.size;
this.activeSessions.clear();
console.log(`Cleared ${count} active sessions`);
}
clearExpiredSessions(maxAge = 3600000) { // 1 hour default
const now = Date.now();
let clearedCount = 0;
for (const [sessionId, data] of this.activeSessions) {
if (now - data.createdAt.getTime() > maxAge) {
this.activeSessions.delete(sessionId);
clearedCount++;
}
}
console.log(`Cleared ${clearedCount} expired sessions`);
}
}
const sessions = new SessionStore();
sessions.addSession('sess_001', { userId: 'user_1' });
sessions.addSession('sess_002', { userId: 'user_2' });
sessions.clearAllSessions(); // Output: Cleared 2 active sessions
Iterating Over Maps #
Maps provide multiple built-in methods for iteration, each optimized for different use cases. Understanding these methods is crucial for working effectively with Maps.
The forEach Method #
The forEach()
method executes a callback for each key-value pair:
class ProductInventory {
constructor() {
this.products = new Map([
['SKU-001', { name: 'Laptop', quantity: 45, price: 1299.99, category: 'Electronics' }],
['SKU-002', { name: 'Mouse', quantity: 150, price: 29.99, category: 'Accessories' }],
['SKU-003', { name: 'Keyboard', quantity: 80, price: 79.99, category: 'Accessories' }],
['SKU-004', { name: 'Monitor', quantity: 30, price: 399.99, category: 'Electronics' }],
['SKU-005', { name: 'Webcam', quantity: 25, price: 89.99, category: 'Electronics' }]
]);
}
generateInventoryReport() {
console.log('=== Inventory Report ===\n');
this.products.forEach((product, sku) => {
const totalValue = product.quantity * product.price;
console.log(`${sku}: ${product.name}`);
console.log(` Category: ${product.category}`);
console.log(` Quantity: ${product.quantity} units`);
console.log(` Unit Price: $${product.price.toFixed(2)}`);
console.log(` Total Value: $${totalValue.toFixed(2)}`);
console.log('---');
});
}
calculateTotalValue() {
let total = 0;
this.products.forEach((product) => {
total += product.quantity * product.price;
});
return total;
}
getLowStockAlerts(threshold = 50) {
const alerts = [];
this.products.forEach((product, sku) => {
if (product.quantity < threshold) {
alerts.push({
sku,
name: product.name,
currentStock: product.quantity,
message: `Low stock alert: Only ${product.quantity} units remaining`
});
}
});
return alerts;
}
}
const inventory = new ProductInventory();
inventory.generateInventoryReport();
console.log(`\nTotal Inventory Value: $${inventory.calculateTotalValue().toFixed(2)}`);
console.log('\nLow Stock Alerts:', inventory.getLowStockAlerts());
Using for…of with entries() #
The entries()
method returns an iterator of [key, value]
pairs:
class SalesAnalytics {
constructor() {
this.salesByRegion = new Map([
['North America', { sales: 1250000, customers: 3500, avgOrder: 357.14 }],
['Europe', { sales: 980000, customers: 2800, avgOrder: 350.00 }],
['Asia Pacific', { sales: 1580000, customers: 4200, avgOrder: 376.19 }],
['Latin America', { sales: 420000, customers: 1200, avgOrder: 350.00 }],
['Middle East', { sales: 315000, customers: 900, avgOrder: 350.00 }]
]);
}
getTopPerformingRegions(topN = 3) {
// Convert to array, sort, and take top N
const sortedRegions = [];
for (const [region, data] of this.salesByRegion.entries()) {
sortedRegions.push({ region, ...data });
}
sortedRegions.sort((a, b) => b.sales - a.sales);
return sortedRegions.slice(0, topN);
}
getRegionsAboveAverage() {
// Calculate average sales
let totalSales = 0;
let regionCount = 0;
for (const [, data] of this.salesByRegion.entries()) {
totalSales += data.sales;
regionCount++;
}
const avgSales = totalSales / regionCount;
// Find regions above average
const aboveAverage = [];
for (const [region, data] of this.salesByRegion.entries()) {
if (data.sales > avgSales) {
aboveAverage.push({
region,
sales: data.sales,
percentageAboveAvg: ((data.sales - avgSales) / avgSales * 100).toFixed(2)
});
}
}
return aboveAverage;
}
calculateGlobalMetrics() {
let totalSales = 0;
let totalCustomers = 0;
for (const [, data] of this.salesByRegion.entries()) {
totalSales += data.sales;
totalCustomers += data.customers;
}
return {
totalSales,
totalCustomers,
globalAvgOrder: totalSales / totalCustomers,
numberOfRegions: this.salesByRegion.size
};
}
}
const analytics = new SalesAnalytics();
console.log('Top 3 Regions:', analytics.getTopPerformingRegions());
console.log('Above Average Regions:', analytics.getRegionsAboveAverage());
console.log('Global Metrics:', analytics.calculateGlobalMetrics());
Iterating Over Keys with keys() #
The keys()
method returns an iterator of all keys:
class PermissionManager {
constructor() {
this.permissions = new Map([
['admin', ['read', 'write', 'delete', 'manage_users', 'manage_settings']],
['editor', ['read', 'write', 'delete']],
['contributor', ['read', 'write']],
['viewer', ['read']],
['guest', []]
]);
}
getAllRoles() {
// Convert keys iterator to array
return Array.from(this.permissions.keys());
}
getRolesSorted() {
const roles = [];
for (const role of this.permissions.keys()) {
roles.push(role);
}
return roles.sort();
}
findRolesWithPermission(permission) {
const rolesWithPermission = [];
for (const role of this.permissions.keys()) {
const rolePermissions = this.permissions.get(role);
if (rolePermissions.includes(permission)) {
rolesWithPermission.push(role);
}
}
return rolesWithPermission;
}
exportRoleNames() {
// Use spread operator with keys()
return [...this.permissions.keys()].join(', ');
}
}
const permissions = new PermissionManager();
console.log('All Roles:', permissions.getAllRoles());
console.log('Sorted Roles:', permissions.getRolesSorted());
console.log('Roles with delete permission:', permissions.findRolesWithPermission('delete'));
console.log('Exported roles:', permissions.exportRoleNames());
Iterating Over Values with values() #
The values()
method returns an iterator of all values:
class OrderProcessor {
constructor() {
this.orders = new Map([
['ORD-001', { items: 5, total: 299.95, status: 'pending', priority: 'normal' }],
['ORD-002', { items: 2, total: 159.98, status: 'processing', priority: 'high' }],
['ORD-003', { items: 8, total: 489.92, status: 'shipped', priority: 'normal' }],
['ORD-004', { items: 1, total: 79.99, status: 'delivered', priority: 'low' }],
['ORD-005', { items: 3, total: 219.97, status: 'pending', priority: 'high' }]
]);
}
calculateTotalRevenue() {
let revenue = 0;
for (const order of this.orders.values()) {
revenue += order.total;
}
return revenue;
}
getTotalItemsOrdered() {
let totalItems = 0;
for (const order of this.orders.values()) {
totalItems += order.items;
}
return totalItems;
}
getOrderStatistics() {
const stats = {
totalOrders: this.orders.size,
totalRevenue: 0,
totalItems: 0,
statusBreakdown: {},
priorityBreakdown: {},
avgOrderValue: 0
};
for (const order of this.orders.values()) {
stats.totalRevenue += order.total;
stats.totalItems += order.items;
// Count by status
stats.statusBreakdown[order.status] =
(stats.statusBreakdown[order.status] || 0) + 1;
// Count by priority
stats.priorityBreakdown[order.priority] =
(stats.priorityBreakdown[order.priority] || 0) + 1;
}
stats.avgOrderValue = stats.totalRevenue / stats.totalOrders;
return stats;
}
getHighPriorityOrders() {
const highPriority = [];
// Combine values() iteration with entries() for full context
for (const [orderId, order] of this.orders.entries()) {
if (order.priority === 'high') {
highPriority.push({ orderId, ...order });
}
}
return highPriority;
}
}
const processor = new OrderProcessor();
console.log('Total Revenue:', processor.calculateTotalRevenue());
console.log('Total Items:', processor.getTotalItemsOrdered());
console.log('Statistics:', processor.getOrderStatistics());
console.log('High Priority Orders:', processor.getHighPriorityOrders());
Advanced Iteration Patterns #
Combining multiple iteration techniques for complex operations:
class DataAnalyzer {
constructor(data) {
this.data = new Map(data);
}
// Filter entries based on a predicate
filter(predicate) {
const filtered = new Map();
for (const [key, value] of this.data.entries()) {
if (predicate(value, key)) {
filtered.set(key, value);
}
}
return new DataAnalyzer(filtered);
}
// Map values to new values
map(transformer) {
const mapped = new Map();
for (const [key, value] of this.data.entries()) {
mapped.set(key, transformer(value, key));
}
return new DataAnalyzer(mapped);
}
// Reduce to a single value
reduce(reducer, initialValue) {
let accumulator = initialValue;
for (const [key, value] of this.data.entries()) {
accumulator = reducer(accumulator, value, key);
}
return accumulator;
}
// Group by a property
groupBy(keySelector) {
const groups = new Map();
for (const [key, value] of this.data.entries()) {
const groupKey = keySelector(value, key);
if (!groups.has(groupKey)) {
groups.set(groupKey, []);
}
groups.get(groupKey).push({ key, value });
}
return groups;
}
toArray() {
return Array.from(this.data.entries());
}
}
// Example usage
const salesData = new DataAnalyzer([
['sale_001', { amount: 1500, region: 'North', product: 'Laptop' }],
['sale_002', { amount: 800, region: 'South', product: 'Mouse' }],
['sale_003', { amount: 2200, region: 'North', product: 'Monitor' }],
['sale_004', { amount: 950, region: 'East', product: 'Keyboard' }],
['sale_005', { amount: 1800, region: 'North', product: 'Laptop' }]
]);
// Filter sales above $1000
const highValueSales = salesData.filter(sale => sale.amount > 1000);
console.log('High value sales:', highValueSales.toArray());
// Group by region
const salesByRegion = salesData.groupBy(sale => sale.region);
console.log('Sales by region:', salesByRegion);
// Calculate total sales
const totalSales = salesData.reduce((sum, sale) => sum + sale.amount, 0);
console.log('Total sales:', totalSales);
// Transform data (add 10% tax)
const salesWithTax = salesData.map(sale => ({
...sale,
amountWithTax: sale.amount * 1.10
}));
console.log('Sales with tax:', salesWithTax.toArray());
Real-World Applications and Patterns #
Let’s explore practical applications where Maps excel, demonstrating their power in solving real-world problems.
Application 1: Advanced Caching System with TTL and LRU #
class AdvancedCache {
constructor(options = {}) {
this.cache = new Map();
this.accessTimes = new Map();
this.expirationTimes = new Map();
this.maxSize = options.maxSize || 1000;
this.defaultTTL = options.defaultTTL || 300000; // 5 minutes
this.evictionPolicy = options.evictionPolicy || 'LRU'; // LRU or LFU
this.accessCounts = new Map(); // For LFU
// Start cleanup interval
if (options.cleanupInterval) {
this.startCleanupInterval(options.cleanupInterval);
}
}
set(key, value, ttl = this.defaultTTL) {
// Evict if at capacity
if (this.cache.size >= this.maxSize && !this.cache.has(key)) {
this.evict();
}
const now = Date.now();
this.cache.set(key, value);
this.accessTimes.set(key, now);
this.expirationTimes.set(key, now + ttl);
this.accessCounts.set(key, 1);
return this;
}
get(key) {
// Check if key exists
if (!this.cache.has(key)) {
return undefined;
}
// Check if expired
if (this.isExpired(key)) {
this.delete(key);
return undefined;
}
// Update access tracking
const now = Date.now();
this.accessTimes.set(key, now);
this.accessCounts.set(key, (this.accessCounts.get(key) || 0) + 1);
return this.cache.get(key);
}
has(key) {
if (!this.cache.has(key)) {
return false;
}
if (this.isExpired(key)) {
this.delete(key);
return false;
}
return true;
}
delete(key) {
this.accessTimes.delete(key);
this.expirationTimes.delete(key);
this.accessCounts.delete(key);
return this.cache.delete(key);
}
clear() {
this.cache.clear();
this.accessTimes.clear();
this.expirationTimes.clear();
this.accessCounts.clear();
}
isExpired(key) {
const expirationTime = this.expirationTimes.get(key);
return expirationTime && Date.now() > expirationTime;
}
evict() {
if (this.evictionPolicy === 'LRU') {
this.evictLRU();
} else if (this.evictionPolicy === 'LFU') {
this.evictLFU();
}
}
evictLRU() {
let oldestKey = null;
let oldestTime = Infinity;
for (const [key, time] of this.accessTimes.entries()) {
if (time < oldestTime) {
oldestTime = time;
oldestKey = key;
}
}
if (oldestKey) {
this.delete(oldestKey);
}
}
evictLFU() {
let leastFrequentKey = null;
let lowestCount = Infinity;
for (const [key, count] of this.accessCounts.entries()) {
if (count < lowestCount) {
lowestCount = count;
leastFrequentKey = key;
}
}
if (leastFrequentKey) {
this.delete(leastFrequentKey);
}
}
cleanupExpired() {
const now = Date.now();
const keysToDelete = [];
for (const [key, expirationTime] of this.expirationTimes.entries()) {
if (now > expirationTime) {
keysToDelete.push(key);
}
}
keysToDelete.forEach(key => this.delete(key));
return keysToDelete.length;
}
startCleanupInterval(interval) {
this.cleanupIntervalId = setInterval(() => {
const cleaned = this.cleanupExpired();
if (cleaned > 0) {
console.log(`Cleaned up ${cleaned} expired cache entries`);
}
}, interval);
}
stopCleanupInterval() {
if (this.cleanupIntervalId) {
clearInterval(this.cleanupIntervalId);
}
}
getStats() {
return {
size: this.cache.size,
maxSize: this.maxSize,
utilizationPercent: (this.cache.size / this.maxSize * 100).toFixed(2),
evictionPolicy: this.evictionPolicy
};
}
}
// Example usage
const cache = new AdvancedCache({
maxSize: 100,
defaultTTL: 60000, // 1 minute
evictionPolicy: 'LRU',
cleanupInterval: 30000 // Clean every 30 seconds
});
// Store user data
cache.set('user:1001', { name: 'Alice', email: 'alice@example.com' });
cache.set('user:1002', { name: 'Bob', email: 'bob@example.com' }, 120000); // 2 minute TTL
// Retrieve data
const user1 = cache.get('user:1001');
console.log('User data:', user1);
// Check stats
console.log('Cache stats:', cache.getStats());
Application 2: Event Emitter with Namespaces #
class EventEmitter {
constructor() {
// Map of event names to Sets of handlers
this.events = new Map();
// Map for one-time handlers
this.onceHandlers = new Map();
// Map for event metadata
this.eventMetadata = new Map();
}
on(eventName, handler, options = {}) {
if (typeof handler !== 'function') {
throw new TypeError('Handler must be a function');
}
if (!this.events.has(eventName)) {
this.events.set(eventName, new Set());
this.eventMetadata.set(eventName, {
emitCount: 0,
lastEmitted: null,
handlerCount: 0
});
}
const handlers = this.events.get(eventName);
handlers.add(handler);
// Update metadata
const metadata = this.eventMetadata.get(eventName);
metadata.handlerCount = handlers.size;
// Store priority if specified
if (options.priority) {
if (!this.handlerPriorities) {
this.handlerPriorities = new Map();
}
if (!this.handlerPriorities.has(eventName)) {
this.handlerPriorities.set(eventName, new Map());
}
this.handlerPriorities.get(eventName).set(handler, options.priority);
}
return this;
}
once(eventName, handler) {
if (!this.onceHandlers.has(eventName)) {
this.onceHandlers.set(eventName, new Set());
}
this.onceHandlers.get(eventName).add(handler);
return this.on(eventName, handler);
}
off(eventName, handler) {
if (!this.events.has(eventName)) {
return this;
}
const handlers = this.events.get(eventName);
handlers.delete(handler);
// Also remove from once handlers if present
if (this.onceHandlers.has(eventName)) {
this.onceHandlers.get(eventName).delete(handler);
}
// Update metadata
const metadata = this.eventMetadata.get(eventName);
metadata.handlerCount = handlers.size;
// Clean up if no handlers remain
if (handlers.size === 0) {
this.events.delete(eventName);
this.onceHandlers.delete(eventName);
}
return this;
}
emit(eventName, ...args) {
if (!this.events.has(eventName)) {
return false;
}
const handlers = this.events.get(eventName);
const onceHandlers = this.onceHandlers.get(eventName) || new Set();
// Sort handlers by priority if priorities exist
let sortedHandlers = Array.from(handlers);
if (this.handlerPriorities && this.handlerPriorities.has(eventName)) {
const priorities = this.handlerPriorities.get(eventName);
sortedHandlers.sort((a, b) => {
const priorityA = priorities.get(a) || 0;
const priorityB = priorities.get(b) || 0;
return priorityB - priorityA; // Higher priority first
});
}
// Execute handlers
for (const handler of sortedHandlers) {
try {
handler(...args);
} catch (error) {
console.error(`Error in event handler for '${eventName}':`, error);
}
// Remove once handlers after execution
if (onceHandlers.has(handler)) {
this.off(eventName, handler);
}
}
// Update metadata
const metadata = this.eventMetadata.get(eventName);
metadata.emitCount++;
metadata.lastEmitted = new Date();
return true;
}
removeAllListeners(eventName) {
if (eventName) {
this.events.delete(eventName);
this.onceHandlers.delete(eventName);
this.eventMetadata.delete(eventName);
} else {
this.events.clear();
this.onceHandlers.clear();
this.eventMetadata.clear();
}
return this;
}
listenerCount(eventName) {
return this.events.has(eventName) ? this.events.get(eventName).size : 0;
}
eventNames() {
return Array.from(this.events.keys());
}
getEventStats(eventName) {
return this.eventMetadata.get(eventName) || null;
}
getAllEventStats() {
const stats = {};
for (const [eventName, metadata] of this.eventMetadata.entries()) {
stats[eventName] = metadata;
}
return stats;
}
}
// Example usage
const emitter = new EventEmitter();
// Register event handlers
emitter.on('user:login', (user) => {
console.log(`User logged in: ${user.name}`);
});
emitter.on('user:login', (user) => {
console.log(`Tracking login for user: ${user.id}`);
}, { priority: 10 }); // Higher priority
emitter.once('user:login', (user) => {
console.log(`First login detected for: ${user.name}`);
});
// Emit events
emitter.emit('user:login', { id: 1001, name: 'Alice' });
emitter.emit('user:login', { id: 1002, name: 'Bob' }); // "First login" won't fire
// Check statistics
console.log('Event stats:', emitter.getAllEventStats());
console.log('Listener count:', emitter.listenerCount('user:login'));
Application 3: Request Rate Limiter #
class RateLimiter {
constructor(options = {}) {
// Map of identifier to request timestamps
this.requests = new Map();
this.maxRequests = options.maxRequests || 100;
this.windowMs = options.windowMs || 60000; // 1 minute default
this.blockDuration = options.blockDuration || 300000; // 5 minutes
// Map of blocked identifiers
this.blockedUntil = new Map();
// Cleanup old entries periodically
this.startCleanup();
}
isAllowed(identifier) {
const now = Date.now();
// Check if blocked
if (this.blockedUntil.has(identifier)) {
const blockExpiry = this.blockedUntil.get(identifier);
if (now < blockExpiry) {
return {
allowed: false,
reason: 'blocked',
retryAfter: Math.ceil((blockExpiry - now) / 1000)
};
} else {
// Unblock
this.blockedUntil.delete(identifier);
this.requests.delete(identifier);
}
}
// Get request history
if (!this.requests.has(identifier)) {
this.requests.set(identifier, []);
}
const timestamps = this.requests.get(identifier);
// Remove old timestamps outside the window
const windowStart = now - this.windowMs;
const recentRequests = timestamps.filter(ts => ts > windowStart);
this.requests.set(identifier, recentRequests);
// Check if limit exceeded
if (recentRequests.length >= this.maxRequests) {
// Block the identifier
this.blockedUntil.set(identifier, now + this.blockDuration);
return {
allowed: false,
reason: 'rate_limit_exceeded',
limit: this.maxRequests,
windowMs: this.windowMs,
retryAfter: Math.ceil(this.blockDuration / 1000)
};
}
// Add current request
recentRequests.push(now);
this.requests.set(identifier, recentRequests);
return {
allowed: true,
remaining: this.maxRequests - recentRequests.length,
resetAt: windowStart + this.windowMs
};
}
reset(identifier) {
this.requests.delete(identifier);
this.blockedUntil.delete(identifier);
}
resetAll() {
this.requests.clear();
this.blockedUntil.clear();
}
getStats(identifier) {
const now = Date.now();
const timestamps = this.requests.get(identifier) || [];
const windowStart = now - this.windowMs;
const recentRequests = timestamps.filter(ts => ts > windowStart);
return {
requestCount: recentRequests.length,
remaining: Math.max(0, this.maxRequests - recentRequests.length),
isBlocked: this.blockedUntil.has(identifier),
resetAt: new Date(windowStart + this.windowMs)
};
}
startCleanup() {
this.cleanupInterval = setInterval(() => {
const now = Date.now();
const windowStart = now - this.windowMs;
// Clean up old request histories
for (const [identifier, timestamps] of this.requests.entries()) {
const recentRequests = timestamps.filter(ts => ts > windowStart);
if (recentRequests.length === 0) {
this.requests.delete(identifier);
} else {
this.requests.set(identifier, recentRequests);
}
}
// Clean up expired blocks
for (const [identifier, blockExpiry] of this.blockedUntil.entries()) {
if (now >= blockExpiry) {
this.blockedUntil.delete(identifier);
}
}
}, 60000); // Clean every minute
}
stopCleanup() {
if (this.cleanupInterval) {
clearInterval(this.cleanupInterval);
}
}
}
// Example usage - API rate limiting
const apiLimiter = new RateLimiter({
maxRequests: 100,
windowMs: 60000, // 1 minute
blockDuration: 300000 // 5 minutes block
});
function handleApiRequest(userId, endpoint) {
const identifier = `${userId}:${endpoint}`;
const result = apiLimiter.isAllowed(identifier);
if (!result.allowed) {
console.log(`Request denied for ${userId} on ${endpoint}`);
console.log(`Reason: ${result.reason}`);
console.log(`Retry after: ${result.retryAfter} seconds`);
return false;
}
console.log(`Request allowed. Remaining: ${result.remaining}`);
// Process the actual request here
return true;
}
// Simulate API requests
for (let i = 0; i < 105; i++) {
handleApiRequest('user_123', '/api/data');
}
// Check stats
console.log('User stats:', apiLimiter.getStats('user_123:/api/data'));
Application 4: Dependency Injection Container #
class DIContainer {
constructor() {
// Map of service names to their definitions
this.services = new Map();
// Map of singleton instances
this.singletons = new Map();
// Map to track service resolution to detect circular dependencies
this.resolving = new Map();
}
register(name, definition, options = {}) {
if (this.services.has(name)) {
throw new Error(`Service '${name}' is already registered`);
}
this.services.set(name, {
definition,
singleton: options.singleton !== false, // Default to singleton
dependencies: options.dependencies || [],
factory: options.factory || false
});
return this;
}
registerFactory(name, factory, options = {}) {
return this.register(name, factory, { ...options, factory: true });
}
registerValue(name, value) {
this.singletons.set(name, value);
return this.register(name, () => value, { singleton: true });
}
resolve(name) {
// Check if already resolving (circular dependency)
if (this.resolving.has(name)) {
const chain = Array.from(this.resolving.keys()).join(' -> ');
throw new Error(`Circular dependency detected: ${chain} -> ${name}`);
}
// Return existing singleton if available
if (this.singletons.has(name)) {
return this.singletons.get(name);
}
// Check if service is registered
if (!this.services.has(name)) {
throw new Error(`Service '${name}' is not registered`);
}
const service = this.services.get(name);
// Mark as resolving
this.resolving.set(name, true);
try {
// Resolve dependencies
const resolvedDeps = service.dependencies.map(dep => this.resolve(dep));
// Create instance
let instance;
if (service.factory) {
instance = service.definition(...resolvedDeps);
} else if (typeof service.definition === 'function') {
instance = new service.definition(...resolvedDeps);
} else {
instance = service.definition;
}
// Store singleton if needed
if (service.singleton) {
this.singletons.set(name, instance);
}
return instance;
} finally {
// Remove from resolving set
this.resolving.delete(name);
}
}
has(name) {
return this.services.has(name);
}
remove(name) {
this.services.delete(name);
this.singletons.delete(name);
return this;
}
clear() {
this.services.clear();
this.singletons.clear();
this.resolving.clear();
}
getRegisteredServices() {
return Array.from(this.services.keys());
}
}
// Example usage - Building an application with DI
class DatabaseConnection {
constructor(config) {
this.config = config;
console.log('Database connection created');
}
query(sql) {
console.log(`Executing query: ${sql}`);
return { rows: [] };
}
}
class UserRepository {
constructor(db) {
this.db = db;
console.log('UserRepository created');
}
findById(id) {
return this.db.query(`SELECT * FROM users WHERE id = ${id}`);
}
findAll() {
return this.db.query('SELECT * FROM users');
}
}
class UserService {
constructor(userRepo, logger) {
this.userRepo = userRepo;
this.logger = logger;
console.log('UserService created');
}
getUser(id) {
this.logger.log(`Fetching user ${id}`);
return this.userRepo.findById(id);
}
getAllUsers() {
this.logger.log('Fetching all users');
return this.userRepo.findAll();
}
}
class Logger {
log(message) {
console.log(`[LOG] ${new Date().toISOString()}: ${message}`);
}
}
// Set up the DI container
const container = new DIContainer();
// Register services
container.registerValue('dbConfig', {
host: 'localhost',
port: 5432,
database: 'myapp'
});
container.register('database', DatabaseConnection, {
dependencies: ['dbConfig'],
singleton: true
});
container.register('logger', Logger, {
singleton: true
});
container.register('userRepository', UserRepository, {
dependencies: ['database'],
singleton: true
});
container.register('userService', UserService, {
dependencies: ['userRepository', 'logger'],
singleton: true
});
// Resolve and use services
const userService = container.resolve('userService');
userService.getUser(123);
userService.getAllUsers();
// List all registered services
console.log('Registered services:', container.getRegisteredServices());
Application 5: Multi-Level Cache with Hierarchical Storage #
class HierarchicalCache {
constructor() {
// L1: In-memory cache (fastest)
this.l1Cache = new Map();
this.l1MaxSize = 100;
this.l1Hits = 0;
// L2: Larger in-memory cache (slower but bigger)
this.l2Cache = new Map();
this.l2MaxSize = 1000;
this.l2Hits = 0;
// Access tracking for LRU
this.accessOrder = new Map();
// Metadata
this.totalRequests = 0;
this.totalMisses = 0;
}
set(key, value, options = {}) {
const priority = options.priority || 'normal'; // high, normal, low
// Always set in L1 for high priority
if (priority === 'high' || this.l1Cache.size < this.l1MaxSize) {
if (this.l1Cache.size >= this.l1MaxSize && !this.l1Cache.has(key)) {
this.evictFromL1();
}
this.l1Cache.set(key, { value, priority, timestamp: Date.now() });
}
// Set in L2
if (this.l2Cache.size >= this.l2MaxSize && !this.l2Cache.has(key)) {
this.evictFromL2();
}
this.l2Cache.set(key, { value, priority, timestamp: Date.now() });
this.accessOrder.set(key, Date.now());
return this;
}
get(key) {
this.totalRequests++;
// Try L1 first
if (this.l1Cache.has(key)) {
this.l1Hits++;
this.accessOrder.set(key, Date.now());
return this.l1Cache.get(key).value;
}
// Try L2
if (this.l2Cache.has(key)) {
this.l2Hits++;
const data = this.l2Cache.get(key);
// Promote to L1 if there's space or if high priority
if (this.l1Cache.size < this.l1MaxSize || data.priority === 'high') {
if (this.l1Cache.size >= this.l1MaxSize) {
this.evictFromL1();
}
this.l1Cache.set(key, data);
}
this.accessOrder.set(key, Date.now());
return data.value;
}
// Cache miss
this.totalMisses++;
return undefined;
}
has(key) {
return this.l1Cache.has(key) || this.l2Cache.has(key);
}
delete(key) {
this.accessOrder.delete(key);
this.l1Cache.delete(key);
return this.l2Cache.delete(key);
}
clear() {
this.l1Cache.clear();
this.l2Cache.clear();
this.accessOrder.clear();
}
evictFromL1() {
// Find least recently used item
let oldestKey = null;
let oldestTime = Infinity;
for (const [key] of this.l1Cache) {
const accessTime = this.accessOrder.get(key);
if (accessTime < oldestTime) {
oldestTime = accessTime;
oldestKey = key;
}
}
if (oldestKey) {
this.l1Cache.delete(oldestKey);
}
}
evictFromL2() {
// Evict lowest priority, then LRU
let evictKey = null;
let lowestPriority = 'high';
let oldestTime = Infinity;
const priorityOrder = { high: 3, normal: 2, low: 1 };
for (const [key, data] of this.l2Cache) {
const accessTime = this.accessOrder.get(key);
const currentPriorityValue = priorityOrder[data.priority];
const lowestPriorityValue = priorityOrder[lowestPriority];
if (currentPriorityValue < lowestPriorityValue ||
(currentPriorityValue === lowestPriorityValue && accessTime < oldestTime)) {
lowestPriority = data.priority;
oldestTime = accessTime;
evictKey = key;
}
}
if (evictKey) {
this.l2Cache.delete(evictKey);
}
}
getStats() {
const l1HitRate = this.totalRequests > 0
? ((this.l1Hits / this.totalRequests) * 100).toFixed(2)
: 0;
const l2HitRate = this.totalRequests > 0
? ((this.l2Hits / this.totalRequests) * 100).toFixed(2)
: 0;
const totalHitRate = this.totalRequests > 0
? (((this.l1Hits + this.l2Hits) / this.totalRequests) * 100).toFixed(2)
: 0;
return {
l1: {
size: this.l1Cache.size,
maxSize: this.l1MaxSize,
hits: this.l1Hits,
hitRate: `${l1HitRate}%`
},
l2: {
size: this.l2Cache.size,
maxSize: this.l2MaxSize,
hits: this.l2Hits,
hitRate: `${l2HitRate}%`
},
overall: {
totalRequests: this.totalRequests,
totalMisses: this.totalMisses,
totalHitRate: `${totalHitRate}%`
}
};
}
inspect() {
console.log('=== L1 Cache ===');
for (const [key, data] of this.l1Cache) {
console.log(`${key}: ${JSON.stringify(data.value)} [${data.priority}]`);
}
console.log('\n=== L2 Cache ===');
for (const [key, data] of this.l2Cache) {
if (!this.l1Cache.has(key)) {
console.log(`${key}: ${JSON.stringify(data.value)} [${data.priority}]`);
}
}
}
}
// Example usage
const cache = new HierarchicalCache();
// Add some data
cache.set('user:1', { name: 'Alice' }, { priority: 'high' });
cache.set('user:2', { name: 'Bob' }, { priority: 'normal' });
cache.set('user:3', { name: 'Charlie' }, { priority: 'low' });
// Simulate access patterns
console.log(cache.get('user:1')); // L1 hit
console.log(cache.get('user:2')); // L1 hit
console.log(cache.get('user:999')); // Miss
// Check statistics
console.log('\nCache Statistics:');
console.log(cache.getStats());
Performance Considerations and Optimization #
Understanding when and how to use Maps efficiently is crucial for building performant applications.
Memory Efficiency #
Maps are generally more memory-efficient than objects for large datasets:
class MemoryComparison {
static compareMemoryUsage(size) {
// Generate test data
const testData = Array.from({ length: size }, (_, i) => [
`key_${i}`,
{ id: i, name: `Item ${i}`, value: Math.random() * 1000 }
]);
// Test Map memory (approximate)
console.time('Map Creation');
const map = new Map(testData);
console.timeEnd('Map Creation');
console.log(`Map size: ${map.size} entries`);
// Test Object memory (approximate)
console.time('Object Creation');
const obj = Object.fromEntries(testData);
console.timeEnd('Object Creation');
console.log(`Object keys: ${Object.keys(obj).length} entries`);
return { map, obj };
}
static compareAccessSpeed(iterations) {
const testData = Array.from({ length: 1000 }, (_, i) => [
`key_${i}`,
{ value: i }
]);
const map = new Map(testData);
const obj = Object.fromEntries(testData);
// Test Map access speed
console.time(`Map ${iterations} accesses`);
for (let i = 0; i < iterations; i++) {
const key = `key_${i % 1000}`;
map.get(key);
}
console.timeEnd(`Map ${iterations} accesses`);
// Test Object access speed
console.time(`Object ${iterations} accesses`);
for (let i = 0; i < iterations; i++) {
const key = `key_${i % 1000}`;
obj[key];
}
console.timeEnd(`Object ${iterations} accesses`);
}
static compareInsertDelete(iterations) {
console.log(`\nTesting ${iterations} insert/delete operations:`);
// Test Map
console.time('Map insert/delete');
const map = new Map();
for (let i = 0; i < iterations; i++) {
map.set(`key_${i}`, i);
}
for (let i = 0; i < iterations; i++) {
map.delete(`key_${i}`);
}
console.timeEnd('Map insert/delete');
// Test Object
console.time('Object insert/delete');
const obj = {};
for (let i = 0; i < iterations; i++) {
obj[`key_${i}`] = i;
}
for (let i = 0; i < iterations; i++) {
delete obj[`key_${i}`];
}
console.timeEnd('Object insert/delete');
}
}
// Run comparisons
console.log('=== Creating 10,000 entries ===');
MemoryComparison.compareMemoryUsage(10000);
console.log('\n=== Access Speed Test ===');
MemoryComparison.compareAccessSpeed(100000);
console.log('\n=== Insert/Delete Performance ===');
MemoryComparison.compareInsertDelete(10000);
When to Use Maps vs Objects #
Understanding the trade-offs helps you make informed decisions:
class DataStructureSelector {
static analyzeUseCase(requirements) {
const scores = {
map: 0,
object: 0,
reasons: {
map: [],
object: []
}
};
// Non-string keys needed
if (requirements.nonStringKeys) {
scores.map += 3;
scores.reasons.map.push('Requires non-string keys');
} else {
scores.object += 1;
scores.reasons.object.push('Only string keys needed');
}
// Frequent additions/deletions
if (requirements.frequentMutations) {
scores.map += 2;
scores.reasons.map.push('Frequent additions/deletions');
}
// Large dataset
if (requirements.largeDataset) {
scores.map += 2;
scores.reasons.map.push('Better performance with large datasets');
}
// Need to iterate in order
if (requirements.orderedIteration) {
scores.map += 2;
scores.reasons.map.push('Guaranteed insertion order');
}
// Need size property
if (requirements.needsSize) {
scores.map += 1;
scores.reasons.map.push('Built-in size property');
}
// JSON serialization needed
if (requirements.jsonSerialization) {
scores.object += 2;
scores.reasons.object.push('Direct JSON serialization');
}
// Simple key-value storage
if (requirements.simpleStorage) {
scores.object += 1;
scores.reasons.object.push('Simple use case');
}
const recommendation = scores.map > scores.object ? 'Map' : 'Object';
return {
recommendation,
mapScore: scores.map,
objectScore: scores.object,
mapReasons: scores.reasons.map,
objectReasons: scores.reasons.object
};
}
}
// Example: Analyze different use cases
const useCase1 = {
nonStringKeys: true,
frequentMutations: true,
largeDataset: true,
orderedIteration: true,
needsSize: true,
jsonSerialization: false,
simpleStorage: false
};
console.log('Use Case 1 (Session Management):');
console.log(DataStructureSelector.analyzeUseCase(useCase1));
const useCase2 = {
nonStringKeys: false,
frequentMutations: false,
largeDataset: false,
orderedIteration: false,
needsSize: false,
jsonSerialization: true,
simpleStorage: true
};
console.log('\nUse Case 2 (Config Storage):');
console.log(DataStructureSelector.analyzeUseCase(useCase2));
Optimizing Map Operations #
Here are practical optimization techniques for working with Maps:
class MapOptimizer {
// Batch operations for better performance
static batchSet(map, entries) {
console.time('Batch Set');
for (const [key, value] of entries) {
map.set(key, value);
}
console.timeEnd('Batch Set');
return map;
}
// Efficient filtering without creating intermediate arrays
static filterInPlace(map, predicate) {
for (const [key, value] of map.entries()) {
if (!predicate(value, key)) {
map.delete(key);
}
}
return map;
}
// Memory-efficient map transformation
static transformValues(map, transformer) {
const result = new Map();
for (const [key, value] of map.entries()) {
result.set(key, transformer(value, key));
}
return result;
}
// Lazy evaluation for large Maps
static *lazyFilter(map, predicate) {
for (const [key, value] of map.entries()) {
if (predicate(value, key)) {
yield [key, value];
}
}
}
// Efficient merging of multiple Maps
static merge(...maps) {
const result = new Map();
for (const map of maps) {
for (const [key, value] of map.entries()) {
result.set(key, value);
}
}
return result;
}
// Deep merge with conflict resolution
static deepMerge(map1, map2, conflictResolver = (v1, v2) => v2) {
const result = new Map(map1);
for (const [key, value] of map2.entries()) {
if (result.has(key)) {
result.set(key, conflictResolver(result.get(key), value, key));
} else {
result.set(key, value);
}
}
return result;
}
}
// Example usage
const map1 = new Map([
['user:1', { score: 100 }],
['user:2', { score: 200 }],
['user:3', { score: 150 }]
]);
const map2 = new Map([
['user:2', { score: 250 }],
['user:4', { score: 300 }]
]);
// Merge with custom conflict resolution
const merged = MapOptimizer.deepMerge(map1, map2, (v1, v2) => ({
score: Math.max(v1.score, v2.score)
}));
console.log('Merged Map:', merged);
// Lazy filtering for memory efficiency
const highScorers = MapOptimizer.lazyFilter(merged, user => user.score > 150);
for (const [userId, data] of highScorers) {
console.log(`${userId}: ${data.score}`);
}
Advanced Patterns and Techniques #
Let’s explore sophisticated patterns that leverage Map’s unique capabilities.
Pattern 1: Memoization with Maps #
class Memoizer {
constructor(options = {}) {
this.cache = new Map();
this.maxSize = options.maxSize || Infinity;
this.ttl = options.ttl || Infinity;
this.keySerializer = options.keySerializer || JSON.stringify;
}
memoize(fn) {
return (...args) => {
const key = this.keySerializer(args);
// Check cache
if (this.cache.has(key)) {
const cached = this.cache.get(key);
// Check if expired
if (Date.now() - cached.timestamp < this.ttl) {
cached.hits++;
return cached.value;
}
this.cache.delete(key);
}
// Compute result
const result = fn(...args);
// Evict if at capacity
if (this.cache.size >= this.maxSize) {
// Remove least recently used
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
// Store in cache
this.cache.set(key, {
value: result,
timestamp: Date.now(),
hits: 0
});
return result;
};
}
clear() {
this.cache.clear();
}
getStats() {
const stats = {
size: this.cache.size,
entries: []
};
for (const [key, data] of this.cache.entries()) {
stats.entries.push({
key,
hits: data.hits,
age: Date.now() - data.timestamp
});
}
return stats;
}
}
// Example: Memoize expensive computation
const memoizer = new Memoizer({ maxSize: 100, ttl: 60000 });
const fibonacci = memoizer.memoize((n) => {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
});
console.time('First call');
console.log('Fibonacci(35):', fibonacci(35));
console.timeEnd('First call');
console.time('Cached call');
console.log('Fibonacci(35):', fibonacci(35));
console.timeEnd('Cached call');
console.log('Cache stats:', memoizer.getStats());
Pattern 2: Composite Keys with Maps #
class CompositeKeyMap {
constructor(keyFields) {
this.map = new Map();
this.keyFields = keyFields;
}
createKey(...values) {
if (values.length !== this.keyFields.length) {
throw new Error(`Expected ${this.keyFields.length} key values`);
}
return values.join('::');
}
set(...args) {
const values = args.slice(0, -1);
const data = args[args.length - 1];
const key = this.createKey(...values);
this.map.set(key, data);
return this;
}
get(...values) {
const key = this.createKey(...values);
return this.map.get(key);
}
has(...values) {
const key = this.createKey(...values);
return this.map.has(key);
}
delete(...values) {
const key = this.createKey(...values);
return this.map.delete(key);
}
findByPartialKey(fieldIndex, value) {
const results = [];
const fieldName = this.keyFields[fieldIndex];
for (const [key, data] of this.map.entries()) {
const keyParts = key.split('::');
if (keyParts[fieldIndex] === String(value)) {
results.push({
key: this.parseKey(key),
data
});
}
}
return results;
}
parseKey(key) {
const values = key.split('::');
const parsed = {};
this.keyFields.forEach((field, index) => {
parsed[field] = values[index];
});
return parsed;
}
entries() {
const result = [];
for (const [key, data] of this.map.entries()) {
result.push({
key: this.parseKey(key),
data
});
}
return result;
}
get size() {
return this.map.size;
}
}
// Example: Multi-dimensional data storage
const salesMap = new CompositeKeyMap(['region', 'product', 'year']);
// Store sales data with composite keys
salesMap.set('North', 'Laptop', '2024', { revenue: 500000, units: 250 });
salesMap.set('North', 'Mouse', '2024', { revenue: 15000, units: 500 });
salesMap.set('South', 'Laptop', '2024', { revenue: 450000, units: 225 });
salesMap.set('North', 'Laptop', '2023', { revenue: 480000, units: 240 });
// Retrieve specific data
console.log('North Laptop 2024:', salesMap.get('North', 'Laptop', '2024'));
// Find all sales for a region
console.log('All North sales:', salesMap.findByPartialKey(0, 'North'));
// Find all laptop sales
console.log('All laptop sales:', salesMap.findByPartialKey(1, 'Laptop'));
Pattern 3: Bidirectional Map #
class BiMap {
constructor(entries = []) {
this.keyToValue = new Map();
this.valueToKey = new Map();
for (const [key, value] of entries) {
this.set(key, value);
}
}
set(key, value) {
// Remove old mappings if they exist
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);
}
// Set new mappings
this.keyToValue.set(key, value);
this.valueToKey.set(value, key);
return this;
}
get(key) {
return this.keyToValue.get(key);
}
getKey(value) {
return this.valueToKey.get(value);
}
has(key) {
return this.keyToValue.has(key);
}
hasValue(value) {
return this.valueToKey.has(value);
}
delete(key) {
if (this.keyToValue.has(key)) {
const value = this.keyToValue.get(key);
this.valueToKey.delete(value);
return this.keyToValue.delete(key);
}
return false;
}
deleteByValue(value) {
if (this.valueToKey.has(value)) {
const key = this.valueToKey.get(value);
this.keyToValue.delete(key);
return this.valueToKey.delete(value);
}
return false;
}
clear() {
this.keyToValue.clear();
this.valueToKey.clear();
}
get size() {
return this.keyToValue.size;
}
entries() {
return this.keyToValue.entries();
}
keys() {
return this.keyToValue.keys();
}
values() {
return this.keyToValue.values();
}
inverse() {
return new BiMap([...this.valueToKey.entries()]);
}
}
// Example: User ID to Username mapping
const userMap = new BiMap([
[1001, 'alice'],
[1002, 'bob'],
[1003, 'charlie']
]);
console.log('User 1001:', userMap.get(1001)); // 'alice'
console.log('Username alice:', userMap.getKey('alice')); // 1001
// Update mapping
userMap.set(1001, 'alice_updated');
console.log('Updated user 1001:', userMap.get(1001)); // 'alice_updated'
console.log('Old username exists?', userMap.hasValue('alice')); // false
// Get inverse map
const usernameToId = userMap.inverse();
console.log('Inverse map - alice_updated:', usernameToId.get('alice_updated')); // 1001
Pattern 4: Multi-Value Map #
class MultiMap {
constructor() {
this.map = new Map();
}
set(key, value) {
if (!this.map.has(key)) {
this.map.set(key, new Set());
}
this.map.get(key).add(value);
return this;
}
get(key) {
return this.map.has(key) ? Array.from(this.map.get(key)) : [];
}
getSet(key) {
return this.map.get(key) || new Set();
}
has(key, value = undefined) {
if (value === undefined) {
return this.map.has(key);
}
return this.map.has(key) && this.map.get(key).has(value);
}
delete(key, value = undefined) {
if (!this.map.has(key)) {
return false;
}
if (value === undefined) {
return this.map.delete(key);
}
const values = this.map.get(key);
const deleted = values.delete(value);
if (values.size === 0) {
this.map.delete(key);
}
return deleted;
}
clear() {
this.map.clear();
}
size() {
let total = 0;
for (const values of this.map.values()) {
total += values.size;
}
return total;
}
keySize() {
return this.map.size;
}
entries() {
const result = [];
for (const [key, values] of this.map.entries()) {
for (const value of values) {
result.push([key, value]);
}
}
return result;
}
keys() {
return Array.from(this.map.keys());
}
values() {
const result = [];
for (const values of this.map.values()) {
result.push(...values);
}
return result;
}
forEach(callback) {
for (const [key, values] of this.map.entries()) {
for (const value of values) {
callback(value, key, this);
}
}
}
}
// Example: Tag system for articles
const articleTags = new MultiMap();
articleTags.set('article_1', 'javascript');
articleTags.set('article_1', 'programming');
articleTags.set('article_1', 'web-development');
articleTags.set('article_2', 'python');
articleTags.set('article_2', 'programming');
articleTags.set('article_2', 'data-science');
console.log('Article 1 tags:', articleTags.get('article_1'));
console.log('Article 2 tags:', articleTags.get('article_2'));
console.log('Has programming tag?', articleTags.has('article_1', 'programming'));
// Remove a specific tag
articleTags.delete('article_1', 'web-development');
console.log('Article 1 tags after deletion:', articleTags.get('article_1'));
console.log('Total entries:', articleTags.size());
console.log('Total keys:', articleTags.keySize());
Converting Between Maps and Other Data Structures #
Understanding how to convert between Maps and other structures is essential for interoperability.
Maps ↔ Objects #
class MapConverter {
// Convert Map to Object
static mapToObject(map) {
const obj = {};
for (const [key, value] of map.entries()) {
// Only use keys that can be object property names
if (typeof key === 'string' || typeof key === 'number' || typeof key === 'symbol') {
// Recursively convert nested Maps
if (value instanceof Map) {
obj[key] = this.mapToObject(value);
} else {
obj[key] = value;
}
}
}
return obj;
}
// Convert Object to Map
static objectToMap(obj) {
const map = new Map();
for (const [key, value] of Object.entries(obj)) {
// Recursively convert nested objects
if (value && typeof value === 'object' && !Array.isArray(value)) {
map.set(key, this.objectToMap(value));
} else {
map.set(key, value);
}
}
return map;
}
// Convert Map to JSON string
static mapToJSON(map) {
const obj = this.mapToObject(map);
return JSON.stringify(obj, null, 2);
}
// Parse JSON to Map
static jsonToMap(jsonString) {
const obj = JSON.parse(jsonString);
return this.objectToMap(obj);
}
// Convert Map to Array of entries
static mapToArray(map) {
return Array.from(map.entries());
}
// Convert Array of entries to Map
static arrayToMap(array) {
return new Map(array);
}
}
// Example usage
const configMap = new Map([
['database', new Map([
['host', 'localhost'],
['port', 5432],
['name', 'mydb']
])],
['cache', new Map([
['enabled', true],
['ttl', 3600]
])]
]);
// Convert to object
const configObj = MapConverter.mapToObject(configMap);
console.log('Config as object:', configObj);
// Convert to JSON
const configJSON = MapConverter.mapToJSON(configMap);
console.log('Config as JSON:', configJSON);
// Parse back to Map
const parsedMap = MapConverter.jsonToMap(configJSON);
console.log('Parsed back to Map:', parsedMap);
Maps ↔ Arrays #
class MapArrayConverter {
// Convert Map to array of objects
static mapToObjectArray(map) {
return Array.from(map.entries()).map(([key, value]) => ({
key,
value
}));
}
// Convert array of objects to Map
static objectArrayToMap(array, keyField = 'key', valueField = 'value') {
const map = new Map();
for (const item of array) {
map.set(item[keyField], item[valueField]);
}
return map;
}
// Group array by key
static groupBy(array, keySelector) {
const map = new Map();
for (const item of array) {
const key = typeof keySelector === 'function'
? keySelector(item)
: item[keySelector];
if (!map.has(key)) {
map.set(key, []);
}
map.get(key).push(item);
}
return map;
}
// Create index from array
static indexBy(array, keySelector) {
const map = new Map();
for (const item of array) {
const key = typeof keySelector === 'function'
? keySelector(item)
: item[keySelector];
map.set(key, item);
}
return map;
}
// Flatten Map to array
static flatten(map, includeKeys = false) {
const result = [];
for (const [key, value] of map.entries()) {
if (Array.isArray(value)) {
if (includeKeys) {
result.push(...value.map(v => ({ key, value: v })));
} else {
result.push(...value);
}
} else {
result.push(includeKeys ? { key, value } : value);
}
}
return result;
}
}
// Example: Working with user data
const users = [
{ id: 1, name: 'Alice', department: 'Engineering', role: 'Developer' },
{ id: 2, name: 'Bob', department: 'Engineering', role: 'Manager' },
{ id: 3, name: 'Charlie', department: 'Sales', role: 'Representative' },
{ id: 4, name: 'Diana', department: 'Sales', role: 'Manager' },
{ id: 5, name: 'Eve', department: 'Engineering', role: 'Developer' }
];
// Group by department
const byDepartment = MapArrayConverter.groupBy(users, 'department');
console.log('Users by department:', byDepartment);
// Index by ID for fast lookup
const userIndex = MapArrayConverter.indexBy(users, 'id');
console.log('User with ID 3:', userIndex.get(3));
// Flatten grouped data
const flattened = MapArrayConverter.flatten(byDepartment, true);
console.log('Flattened data:', flattened);
WeakMap: Garbage Collection-Friendly Maps #
WeakMap is a special type of Map that allows keys to be garbage collected when no other references exist.
// Example: Storing private data with WeakMap
class UserManager {
constructor() {
// WeakMap for private data
this.privateData = new WeakMap();
this.users = [];
}
createUser(name, email) {
const user = { name, email };
// Store sensitive data in WeakMap
this.privateData.set(user, {
password: this.hashPassword('temp123'),
ssn: '***-**-****',
createdAt: new Date()
});
this.users.push(user);
return user;
}
hashPassword(password) {
// Simplified hash
return `hashed_${password}`;
}
getPrivateData(user) {
return this.privateData.get(user);
}
deleteUser(user) {
const index = this.users.indexOf(user);
if (index > -1) {
this.users.splice(index, 1);
// Private data will be garbage collected automatically
// when no references to user object exist
}
}
}
// Example: DOM element metadata with WeakMap
class ElementTracker {
constructor() {
this.metadata = new WeakMap();
}
track(element, data) {
this.metadata.set(element, {
...data,
trackedAt: new Date()
});
}
getData(element) {
return this.metadata.get(element);
}
updateClicks(element) {
const data = this.metadata.get(element);
if (data) {
data.clicks = (data.clicks || 0) + 1;
data.lastClicked = new Date();
}
}
}
// Usage
const tracker = new ElementTracker();
const button = document.createElement('button');
tracker.track(button, { name: 'submit-button', clicks: 0 });
tracker.updateClicks(button);
console.log('Button data:', tracker.getData(button));
// When button is removed and no references exist,
// the metadata will be automatically garbage collected
Best Practices and Common Pitfalls #
Best Practices #
class MapBestPractices {
// 1. Use Maps for frequent additions/deletions
static cacheExample() {
const cache = new Map();
// Efficient addition
cache.set('key1', 'value1');
// Efficient deletion
cache.delete('key1');
return cache;
}
// 2. Use for...of for iteration (more efficient)
static efficientIteration(map) {
// Good: Direct iteration
for (const [key, value] of map) {
console.log(key, value);
}
// Avoid: Converting to array first (unless needed)
// Array.from(map).forEach(([key, value]) => { ... });
}
// 3. Check existence before getting
static safeAccess(map, key, defaultValue = null) {
return map.has(key) ? map.get(key) : defaultValue;
}
// 4. Use Map.prototype.size instead of converting to array
static checkSize(map) {
// Good
if (map.size > 100) {
console.log('Large map');
}
// Avoid
// if (Array.from(map).length > 100) { ... }
}
// 5. Clear Maps when done to free memory
static cleanup(map) {
// Process data
for (const [key, value] of map) {
// ... process
}
// Clear when done
map.clear();
}
// 6. Use WeakMap for object keys that should be garbage collected
static useWeakMapForDOM() {
const elementData = new WeakMap();
const div = document.createElement('div');
elementData.set(div, { clicks: 0 });
// When div is removed, data is auto-cleaned
return elementData;
}
// 7. Serialize Maps properly for storage
static serializeMap(map) {
return JSON.stringify({
dataType: 'Map',
value: Array.from(map.entries())
});
}
static deserializeMap(jsonString) {
const data = JSON.parse(jsonString);
if (data.dataType === 'Map') {
return new Map(data.value);
}
throw new Error('Invalid Map data');
}
}
Common Pitfalls to Avoid #
class MapPitfalls {
// Pitfall 1: Using objects as keys without understanding reference equality
static objectKeyPitfall() {
const map = new Map();
const key1 = { id: 1 };
const key2 = { id: 1 }; // Different object, same content
map.set(key1, 'value1');
console.log(map.get(key1)); // 'value1'
console.log(map.get(key2)); // undefined (different reference!)
// Solution: Use the same reference or use string keys
const idMap = new Map();
idMap.set('1', 'value1');
console.log(idMap.get('1')); // 'value1'
}
// Pitfall 2: Forgetting Maps are not directly JSON-serializable
static jsonPitfall() {
const map = new Map([['key1', 'value1']]);
// Wrong: This doesn't preserve the Map
console.log(JSON.stringify(map)); // '{}'
// Right: Convert to array first
const serialized = JSON.stringify(Array.from(map.entries()));
const deserialized = new Map(JSON.parse(serialized));
console.log(deserialized.get('key1')); // 'value1'
}
// Pitfall 3: Mutating values without re-setting
static mutationPitfall() {
const map = new Map();
const obj = { count: 0 };
map.set('key', obj);
// This works because we're mutating the object
obj.count++;
console.log(map.get('key').count); // 1
// But this doesn't update the map
const newObj = { count: 10 };
// map.get('key') = newObj; // This doesn't work
// Correct way:
map.set('key', newObj);
console.log(map.get('key').count); // 10
}
// Pitfall 4: Not handling undefined vs non-existent keys
static undefinedPitfall() {
const map = new Map();
map.set('key1', undefined);
// Both return undefined!
console.log(map.get('key1')); // undefined (exists, value is undefined)
console.log(map.get('key2')); // undefined (doesn't exist)
// Solution: Use has() to check existence
console.log(map.has('key1')); // true
console.log(map.has('key2')); // false
}
// Pitfall 5: Memory leaks with large Maps
static memoryLeakPitfall() {
const cache = new Map();
// Bad: Unlimited growth
function addToCacheBad(key, value) {
cache.set(key, value);
}
// Good: Implement size limits
function addToCacheGood(key, value, maxSize = 1000) {
if (cache.size >= maxSize) {
// Remove oldest entry
const firstKey = cache.keys().next().value;
cache.delete(firstKey);
}
cache.set(key, value);
}
}
// Pitfall 6: Using Maps when Objects would be simpler
static oversimplificationPitfall() {
// Overkill for simple config
const configMap = new Map([
['apiUrl', 'https://api.example.com'],
['timeout', 5000]
]);
// Better: Use an object for simple string-keyed config
const configObj = {
apiUrl: 'https://api.example.com',
timeout: 5000
};
// Use Maps when you need the extra features:
// - Non-string keys
// - Frequent additions/deletions
// - Iteration order guarantee
// - Size property
}
}
// Run examples
console.log('=== Object Key Pitfall ===');
MapPitfalls.objectKeyPitfall();
console.log('\n=== JSON Serialization Pitfall ===');
MapPitfalls.jsonPitfall();
console.log('\n=== Undefined vs Non-existent ===');
MapPitfalls.undefinedPitfall();
Practical Example: Building a Complete Application Feature #
Let’s build a comprehensive user session management system that demonstrates Maps in a real-world context:
class SessionManagementSystem {
constructor(options = {}) {
// Active sessions: userId -> session data
this.activeSessions = new Map();
// Session tokens: token -> userId
this.tokenToUser = new Map();
// User activity tracking: userId -> activity log
this.userActivity = new Map();
// Failed login attempts: userId -> attempt data
this.failedAttempts = new Map();
// Session statistics
this.stats = {
totalLogins: 0,
totalLogouts: 0,
activeSessionCount: 0,
failedLoginCount: 0
};
// Configuration
this.config = {
sessionTimeout: options.sessionTimeout || 1800000, // 30 minutes
maxSessions: options.maxSessions || 5,
maxFailedAttempts: options.maxFailedAttempts || 5,
lockoutDuration: options.lockoutDuration || 900000 // 15 minutes
};
// Start cleanup process
this.startMaintenanceTasks();
}
generateToken() {
return `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
async login(userId, credentials, metadata = {}) {
// Check if user is locked out
if (this.isLockedOut(userId)) {
const lockoutTime = this.failedAttempts.get(userId).lockedUntil;
const remainingTime = Math.ceil((lockoutTime - Date.now()) / 1000);
return {
success: false,
error: 'account_locked',
message: `Account locked. Try again in ${remainingTime} seconds.`
};
}
// Validate credentials (simplified)
const isValid = await this.validateCredentials(userId, credentials);
if (!isValid) {
this.recordFailedAttempt(userId);
this.stats.failedLoginCount++;
const attempts = this.failedAttempts.get(userId);
const remaining = this.config.maxFailedAttempts - attempts.count;
return {
success: false,
error: 'invalid_credentials',
message: `Invalid credentials. ${remaining} attempts remaining.`
};
}
// Clear failed attempts on successful login
this.failedAttempts.delete(userId);
// Check max concurrent sessions
if (this.getUserSessionCount(userId) >= this.config.maxSessions) {
this.removeOldestSession(userId);
}
// Create new session
const token = this.generateToken();
const sessionData = {
userId,
token,
createdAt: new Date(),
lastActivity: new Date(),
expiresAt: new Date(Date.now() + this.config.sessionTimeout),
ipAddress: metadata.ipAddress,
userAgent: metadata.userAgent,
deviceInfo: metadata.deviceInfo
};
// Store session
this.activeSessions.set(token, sessionData);
this.tokenToUser.set(token, userId);
// Initialize activity log
if (!this.userActivity.has(userId)) {
this.userActivity.set(userId, []);
}
this.logActivity(userId, 'login', { token, metadata });
// Update stats
this.stats.totalLogins++;
this.stats.activeSessionCount = this.activeSessions.size;
return {
success: true,
token,
expiresAt: sessionData.expiresAt
};
}
validateCredentials(userId, credentials) {
// Simplified validation - in real app, check against database
return new Promise(resolve => {
setTimeout(() => {
resolve(credentials.password === 'correct_password');
}, 100);
});
}
recordFailedAttempt(userId) {
if (!this.failedAttempts.has(userId)) {
this.failedAttempts.set(userId, {
count: 0,
attempts: [],
lockedUntil: null
});
}
const attemptData = this.failedAttempts.get(userId);
attemptData.count++;
attemptData.attempts.push({
timestamp: new Date(),
ipAddress: '0.0.0.0' // Would get from request
});
// Lock account if too many attempts
if (attemptData.count >= this.config.maxFailedAttempts) {
attemptData.lockedUntil = Date.now() + this.config.lockoutDuration;
this.logActivity(userId, 'account_locked', {
reason: 'too_many_failed_attempts',
unlockAt: new Date(attemptData.lockedUntil)
});
}
}
isLockedOut(userId) {
if (!this.failedAttempts.has(userId)) {
return false;
}
const attemptData = this.failedAttempts.get(userId);
if (attemptData.lockedUntil && Date.now() < attemptData.lockedUntil) {
return true;
}
// Unlock if time has passed
if (attemptData.lockedUntil && Date.now() >= attemptData.lockedUntil) {
this.failedAttempts.delete(userId);
return false;
}
return false;
}
validateSession(token) {
if (!this.activeSessions.has(token)) {
return { valid: false, error: 'session_not_found' };
}
const session = this.activeSessions.get(token);
// Check expiration
if (Date.now() > session.expiresAt.getTime()) {
this.logout(token);
return { valid: false, error: 'session_expired' };
}
// Update last activity
session.lastActivity = new Date();
// Extend expiration
session.expiresAt = new Date(Date.now() + this.config.sessionTimeout);
return { valid: true, session };
}
logout(token) {
if (!this.activeSessions.has(token)) {
return { success: false, error: 'session_not_found' };
}
const session = this.activeSessions.get(token);
const userId = session.userId;
// Remove session
this.activeSessions.delete(token);
this.tokenToUser.delete(token);
// Log activity
this.logActivity(userId, 'logout', { token });
// Update stats
this.stats.totalLogouts++;
this.stats.activeSessionCount = this.activeSessions.size;
return { success: true };
}
logoutAllSessions(userId) {
let loggedOut = 0;
for (const [token, session] of this.activeSessions.entries()) {
if (session.userId === userId) {
this.activeSessions.delete(token);
this.tokenToUser.delete(token);
loggedOut++;
}
}
if (loggedOut > 0) {
this.logActivity(userId, 'logout_all', { sessionsTerminated: loggedOut });
}
this.stats.activeSessionCount = this.activeSessions.size;
return { success: true, sessionsTerminated: loggedOut };
}
getUserSessionCount(userId) {
let count = 0;
for (const session of this.activeSessions.values()) {
if (session.userId === userId) {
count++;
}
}
return count;
}
getUserSessions(userId) {
const sessions = [];
for (const [token, session] of this.activeSessions.entries()) {
if (session.userId === userId) {
sessions.push({
token,
createdAt: session.createdAt,
lastActivity: session.lastActivity,
expiresAt: session.expiresAt,
deviceInfo: session.deviceInfo
});
}
}
return sessions;
}
removeOldestSession(userId) {
let oldestToken = null;
let oldestTime = Infinity;
for (const [token, session] of this.activeSessions.entries()) {
if (session.userId === userId) {
const createdTime = session.createdAt.getTime();
if (createdTime < oldestTime) {
oldestTime = createdTime;
oldestToken = token;
}
}
}
if (oldestToken) {
this.logout(oldestToken);
}
}
logActivity(userId, action, details = {}) {
if (!this.userActivity.has(userId)) {
this.userActivity.set(userId, []);
}
const activities = this.userActivity.get(userId);
activities.push({
action,
timestamp: new Date(),
details
});
// Keep only last 100 activities
if (activities.length > 100) {
activities.shift();
}
}
getUserActivity(userId, limit = 50) {
const activities = this.userActivity.get(userId) || [];
return activities.slice(-limit);
}
cleanupExpiredSessions() {
const now = Date.now();
let cleaned = 0;
for (const [token, session] of this.activeSessions.entries()) {
if (now > session.expiresAt.getTime()) {
this.activeSessions.delete(token);
this.tokenToUser.delete(token);
this.logActivity(session.userId, 'session_expired', { token });
cleaned++;
}
}
this.stats.activeSessionCount = this.activeSessions.size;
return cleaned;
}
cleanupOldActivityLogs() {
const maxAge = 30 * 24 * 60 * 60 * 1000; // 30 days
const cutoff = Date.now() - maxAge;
let cleaned = 0;
for (const [userId, activities] of this.userActivity.entries()) {
const filtered = activities.filter(
activity => activity.timestamp.getTime() > cutoff
);
if (filtered.length === 0) {
this.userActivity.delete(userId);
} else if (filtered.length < activities.length) {
this.userActivity.set(userId, filtered);
}
cleaned += activities.length - filtered.length;
}
return cleaned;
}
startMaintenanceTasks() {
// Clean expired sessions every 5 minutes
this.maintenanceInterval = setInterval(() => {
const cleaned = this.cleanupExpiredSessions();
if (cleaned > 0) {
console.log(`[Maintenance] Cleaned ${cleaned} expired sessions`);
}
}, 300000); // 5 minutes
// Clean old activity logs daily
this.activityCleanupInterval = setInterval(() => {
const cleaned = this.cleanupOldActivityLogs();
if (cleaned > 0) {
console.log(`[Maintenance] Cleaned ${cleaned} old activity logs`);
}
}, 86400000); // 24 hours
}
stopMaintenanceTasks() {
if (this.maintenanceInterval) {
clearInterval(this.maintenanceInterval);
}
if (this.activityCleanupInterval) {
clearInterval(this.activityCleanupInterval);
}
}
getSystemStats() {
return {
...this.stats,
activeUsers: new Set(
Array.from(this.activeSessions.values()).map(s => s.userId)
).size,
averageSessionsPerUser: this.stats.activeSessionCount /
Math.max(1, new Set(
Array.from(this.activeSessions.values()).map(s => s.userId)
).size)
};
}
getDetailedStats() {
const userSessionCounts = new Map();
for (const session of this.activeSessions.values()) {
const count = userSessionCounts.get(session.userId) || 0;
userSessionCounts.set(session.userId, count + 1);
}
return {
system: this.getSystemStats(),
userSessionDistribution: Array.from(userSessionCounts.entries())
.map(([userId, count]) => ({ userId, sessionCount: count }))
.sort((a, b) => b.sessionCount - a.sessionCount),
lockedAccounts: Array.from(this.failedAttempts.entries())
.filter(([, data]) => data.lockedUntil && Date.now() < data.lockedUntil)
.map(([userId, data]) => ({
userId,
unlockAt: new Date(data.lockedUntil),
failedAttempts: data.count
}))
};
}
shutdown() {
this.stopMaintenanceTasks();
this.activeSessions.clear();
this.tokenToUser.clear();
this.userActivity.clear();
this.failedAttempts.clear();
}
}
// Example usage
async function demonstrateSessionManagement() {
const sessionManager = new SessionManagementSystem({
sessionTimeout: 1800000, // 30 minutes
maxSessions: 3,
maxFailedAttempts: 3,
lockoutDuration: 900000 // 15 minutes
});
// Simulate user login
const loginResult = await sessionManager.login('user_123',
{ password: 'correct_password' },
{
ipAddress: '192.168.1.1',
userAgent: 'Mozilla/5.0...',
deviceInfo: { type: 'desktop', os: 'Windows' }
}
);
console.log('Login result:', loginResult);
if (loginResult.success) {
// Validate session
const validation = sessionManager.validateSession(loginResult.token);
console.log('Session validation:', validation);
// Get user sessions
const sessions = sessionManager.getUserSessions('user_123');
console.log('Active sessions:', sessions);
// Get user activity
const activity = sessionManager.getUserActivity('user_123');
console.log('User activity:', activity);
// Get system stats
const stats = sessionManager.getSystemStats();
console.log('System stats:', stats);
// Logout
const logoutResult = sessionManager.logout(loginResult.token);
console.log('Logout result:', logoutResult);
}
// Simulate failed login attempts
for (let i = 0; i < 4; i++) {
const failedLogin = await sessionManager.login('user_456',
{ password: 'wrong_password' },
{ ipAddress: '192.168.1.2' }
);
console.log(`Failed attempt ${i + 1}:`, failedLogin);
}
// Get detailed stats
const detailedStats = sessionManager.getDetailedStats();
console.log('Detailed system stats:', detailedStats);
// Cleanup
sessionManager.shutdown();
}
// Run demonstration
demonstrateSessionManagement().catch(console.error);
Conclusion #
JavaScript Maps are powerful, versatile data structures that offer significant advantages over plain objects in many scenarios. Throughout this comprehensive guide, we’ve explored Maps from fundamental concepts to advanced real-world applications.
Key Takeaways #
When to Use Maps:
- You need non-string keys (objects, numbers, symbols, functions)
- Frequent additions and deletions are required
- Insertion order must be preserved
- You need a performant size property
- Working with large datasets requiring optimal performance
- Implementing caching systems, event managers, or session stores
When to Use Objects:
- Simple string-keyed configuration data
- Data that needs JSON serialization without conversion
- Fixed structure with known properties
- Interoperability with existing object-based code
Performance Benefits:
- Maps excel at frequent insertions and deletions
- Better performance with large numbers of key-value pairs
- More memory-efficient for dynamic datasets
- Faster iteration compared to objects with many properties
Advanced Capabilities:
- Use WeakMap for automatic garbage collection with object keys
- Implement sophisticated patterns like memoization, dependency injection, and multi-level caching
- Build composite keys and bidirectional mappings
- Create type-safe, maintainable code with clear semantics
Best Practices:
- Clear Maps when done to prevent memory leaks
- Use appropriate iteration methods for your use case
- Handle undefined values explicitly with
has()
checks - Implement size limits for unbounded caches
- Convert properly for JSON serialization
- Choose WeakMap when keys should be garbage collected
Maps represent a fundamental tool in modern JavaScript development. By understanding their strengths, limitations, and appropriate use cases, you can build more performant, maintainable, and elegant applications. Whether you’re implementing a simple cache or building complex data management systems, Maps provide the flexibility and performance needed for production-grade JavaScript applications.
As JavaScript continues to evolve, Maps remain a cornerstone of efficient data structure implementation, offering developers a robust alternative to plain objects with superior performance characteristics and enhanced capabilities for managing key-value relationships in sophisticated applications.