JavaScript Map - Exercise 3

Maps do not have a built in filter method. To filter entries, convert the Map to an array using Array.from() or the spread operator, filter the array, and then create a new Map from the filtered results.

javascript

const scores = new Map([
  ['Alice', 85],
  ['Bob', 42],
  ['Charlie', 91],
  ['Diana', 38]
]);

// Filter entries where score is 50 or higher
const passing = new Map(
  [...scores].filter(([name, score]) => score >= 50)
);

console.log(passing);
// Map(2) { 'Alice' => 85, 'Charlie' => 91 }

// Filter entries where name starts with 'A' or 'B'
const filtered = new Map(
  Array.from(scores).filter(([name]) => /^[AB]/.test(name))
);

console.log(filtered);
// Map(2) { 'Alice' => 85, 'Bob' => 42 }

To find an entry based on a condition, convert the Map entries to an array and use the find() method. This returns the first matching key value pair or undefined if no match is found.

javascript

const products = new Map([
  ['laptop', { price: 999, inStock: true }],
  ['phone', { price: 699, inStock: false }],
  ['tablet', { price: 449, inStock: true }]
]);

// Find first product under $500 that is in stock
const found = [...products.entries()].find(
  ([key, value]) => value.price < 500 && value.inStock
);

console.log(found);
// ['tablet', { price: 449, inStock: true }]

// Find by key pattern
const phoneEntry = [...products].find(([key]) => key.includes('phone'));

console.log(phoneEntry);
// ['phone', { price: 699, inStock: false }]

// Check if entry exists using some()
const hasExpensive = [...products.values()].some(p => p.price > 900);
console.log(hasExpensive); // true

Maps maintain insertion order but do not have a sort method. To sort by keys, convert to an array, sort the array, and create a new Map. The new Map will preserve the sorted order.

javascript

const fruits = new Map([
  ['banana', 3],
  ['apple', 5],
  ['cherry', 2],
  ['date', 8]
]);

// Sort by keys alphabetically (ascending)
const sortedByKey = new Map(
  [...fruits].sort((a, b) => a[0].localeCompare(b[0]))
);

console.log([...sortedByKey.keys()]);
// ['apple', 'banana', 'cherry', 'date']

// Sort by keys in reverse alphabetical order
const sortedDesc = new Map(
  [...fruits].sort((a, b) => b[0].localeCompare(a[0]))
);

console.log([...sortedDesc.keys()]);
// ['date', 'cherry', 'banana', 'apple']

// Sort numeric keys
const numbers = new Map([[3, 'three'], [1, 'one'], [2, 'two']]);
const sortedNums = new Map([...numbers].sort((a, b) => a[0] - b[0]));

console.log([...sortedNums.keys()]); // [1, 2, 3]

To sort a Map by values, convert it to an array of entries, sort based on the value (second element of each entry), and create a new Map from the sorted array.

javascript

const scores = new Map([
  ['Alice', 85],
  ['Bob', 92],
  ['Charlie', 78],
  ['Diana', 95]
]);

// Sort by values ascending
const sortedAsc = new Map(
  [...scores].sort((a, b) => a[1] - b[1])
);

console.log([...sortedAsc]);
// [['Charlie', 78], ['Alice', 85], ['Bob', 92], ['Diana', 95]]

// Sort by values descending (highest first)
const sortedDesc = new Map(
  [...scores].sort((a, b) => b[1] - a[1])
);

console.log([...sortedDesc]);
// [['Diana', 95], ['Bob', 92], ['Alice', 85], ['Charlie', 78]]

// Get top 2 scorers
const top2 = new Map([...sortedDesc].slice(0, 2));
console.log(top2); // Map(2) { 'Diana' => 95, 'Bob' => 92 }

To find common keys between two Maps, iterate over one Map and check if each key exists in the other using the has() method. You can choose which Map's values to keep in the result.

javascript

const map1 = new Map([
  ['a', 1],
  ['b', 2],
  ['c', 3]
]);

const map2 = new Map([
  ['b', 20],
  ['c', 30],
  ['d', 40]
]);

// Get intersection keeping values from map1
const intersection = new Map(
  [...map1].filter(([key]) => map2.has(key))
);

console.log(intersection);
// Map(2) { 'b' => 2, 'c' => 3 }

// Get intersection with values from map2
const intersection2 = new Map(
  [...map2].filter(([key]) => map1.has(key))
);

console.log(intersection2);
// Map(2) { 'b' => 20, 'c' => 30 }

// Get intersection with combined values
const combined = new Map(
  [...map1]
    .filter(([key]) => map2.has(key))
    .map(([key, val]) => [key, { map1: val, map2: map2.get(key) }])
);

console.log(combined);
// Map(2) { 'b' => { map1: 2, map2: 20 }, 'c' => { map1: 3, map2: 30 } }

The difference of two Maps contains entries from the first Map whose keys do not exist in the second Map. Filter the first Map to keep only entries where the key is not found in the second Map.

javascript

const map1 = new Map([
  ['a', 1],
  ['b', 2],
  ['c', 3],
  ['d', 4]
]);

const map2 = new Map([
  ['b', 20],
  ['d', 40]
]);

// Get entries in map1 that are not in map2
const difference = new Map(
  [...map1].filter(([key]) => !map2.has(key))
);

console.log(difference);
// Map(2) { 'a' => 1, 'c' => 3 }

// Get entries in map2 that are not in map1
const difference2 = new Map(
  [...map2].filter(([key]) => !map1.has(key))
);

console.log(difference2);
// Map(0) {} (all map2 keys exist in map1)

// Symmetric difference (entries unique to each Map)
const symmetricDiff = new Map([
  ...[...map1].filter(([key]) => !map2.has(key)),
  ...[...map2].filter(([key]) => !map1.has(key))
]);

console.log(symmetricDiff);
// Map(2) { 'a' => 1, 'c' => 3 }

Maps are excellent for grouping array items by a key. Iterate over the array and use the grouping key to collect items into arrays stored as Map values. This is useful for categorizing data.

javascript

const people = [
  { name: 'Alice', department: 'Engineering' },
  { name: 'Bob', department: 'Marketing' },
  { name: 'Charlie', department: 'Engineering' },
  { name: 'Diana', department: 'Marketing' },
  { name: 'Eve', department: 'Sales' }
];

// Group by department
const grouped = new Map();

for (const person of people) {
  const dept = person.department;
  if (!grouped.has(dept)) {
    grouped.set(dept, []);
  }
  grouped.get(dept).push(person);
}

console.log(grouped.get('Engineering'));
// [{ name: 'Alice', ... }, { name: 'Charlie', ... }]

// Using reduce for grouping
const groupedReduce = people.reduce((map, person) => {
  const dept = person.department;
  const group = map.get(dept) || [];
  group.push(person);
  return map.set(dept, group);
}, new Map());

console.log(groupedReduce.size); // 3 departments

Maps are perfect for counting occurrences of items. Use each unique item as a key and increment its count as you iterate. This pattern is commonly used for frequency analysis and histograms.

javascript

const words = ['apple', 'banana', 'apple', 'cherry', 'banana', 'apple'];

// Count occurrences
const counts = new Map();

for (const word of words) {
  counts.set(word, (counts.get(word) || 0) + 1);
}

console.log(counts);
// Map(3) { 'apple' => 3, 'banana' => 2, 'cherry' => 1 }

// Using reduce
const countReduce = words.reduce((map, word) => {
  return map.set(word, (map.get(word) || 0) + 1);
}, new Map());

// Find most frequent
const mostFrequent = [...countReduce.entries()]
  .sort((a, b) => b[1] - a[1])[0];

console.log(mostFrequent); // ['apple', 3]

// Count characters in a string
const text = 'hello';
const charCounts = new Map();

for (const char of text) {
  charCounts.set(char, (charCounts.get(char) || 0) + 1);
}

console.log(charCounts);
// Map(4) { 'h' => 1, 'e' => 1, 'l' => 2, 'o' => 1 }

Maps work well as caches for memoization. Store computed results with input values as keys. Before computing, check if the result already exists in the Map to avoid redundant calculations.

javascript

// Memoized fibonacci using Map
const fibCache = new Map();

function fibonacci(n) {
  if (n <= 1) return n;
  
  if (fibCache.has(n)) {
    return fibCache.get(n);
  }
  
  const result = fibonacci(n - 1) + fibonacci(n - 2);
  fibCache.set(n, result);
  return result;
}

console.log(fibonacci(40)); // 102334155 (fast with cache)
console.log(fibCache.size); // 39 cached values

// Generic memoization function
function memoize(fn) {
  const cache = new Map();
  
  return function(...args) {
    const key = JSON.stringify(args);
    
    if (cache.has(key)) {
      return cache.get(key);
    }
    
    const result = fn.apply(this, args);
    cache.set(key, result);
    return result;
  };
}

// Usage
const expensiveCalc = memoize((x, y) => {
  console.log('Computing...');
  return x * y;
});

console.log(expensiveCalc(4, 5)); // Computing... 20
console.log(expensiveCalc(4, 5)); // 20 (cached, no "Computing...")

JSON does not directly support Maps. To create a Map from JSON, first parse the JSON into an object or array, then convert it to a Map using Object.entries() or by passing an array of pairs to the Map constructor.

javascript

// From JSON object
const jsonString = '{"name": "Alice", "age": 30, "city": "NYC"}';
const parsed = JSON.parse(jsonString);
const mapFromObject = new Map(Object.entries(parsed));

console.log(mapFromObject);
// Map(3) { 'name' => 'Alice', 'age' => 30, 'city' => 'NYC' }

// From JSON array of pairs
const jsonPairs = '[["a", 1], ["b", 2], ["c", 3]]';
const mapFromPairs = new Map(JSON.parse(jsonPairs));

console.log(mapFromPairs);
// Map(3) { 'a' => 1, 'b' => 2, 'c' => 3 }

// Converting Map back to JSON
const myMap = new Map([['x', 10], ['y', 20]]);

// As object (only works with string keys)
const asObject = Object.fromEntries(myMap);
console.log(JSON.stringify(asObject)); // '{"x":10,"y":20}'

// As array of pairs (preserves any key type)
const asArray = [...myMap];
console.log(JSON.stringify(asArray)); // '[["x",10],["y",20]]'

A WeakMap only accepts objects as keys and holds weak references to them. If no other references to a key exist, it can be garbage collected. WeakMaps are not iterable and have no size property, making them ideal for private data storage.

javascript

// WeakMap only accepts objects as keys
const weakMap = new WeakMap();

let obj = { name: 'Alice' };
weakMap.set(obj, 'some private data');

console.log(weakMap.get(obj)); // 'some private data'

// Primitives as keys will throw an error
// weakMap.set('string', 'value'); // TypeError

// Key differences from Map
const map = new Map();
map.set(obj, 'data');

// Map is iterable, WeakMap is not
console.log(map.size);      // 1
// console.log(weakMap.size); // undefined (no size property)

// Practical use: storing private data
const privateData = new WeakMap();

class User {
  constructor(name, password) {
    this.name = name;
    privateData.set(this, { password });
  }
  
  checkPassword(input) {
    return privateData.get(this).password === input;
  }
}

const user = new User('Alice', 'secret123');
console.log(user.name);                    // 'Alice'
console.log(user.checkPassword('secret123')); // true
console.log(user.password);                // undefined (private)

Use Map when you need non string keys, frequent additions and deletions, ordered iteration, or need to track the size easily. Objects are better for static structures with string keys and when you need JSON serialization.

javascript

// Use Map when keys are not strings
const userActions = new Map();
const button1 = document.createElement('button');
const button2 = document.createElement('button');

userActions.set(button1, { clicks: 5 });
userActions.set(button2, { clicks: 3 });

// Use Map for frequent additions/deletions
const cache = new Map();
cache.set('key1', 'value1');
cache.delete('key1'); // More performant than object

// Use Map when you need size
console.log(cache.size); // 0
// vs Object.keys(obj).length

// Use Map for ordered iteration
const orderedMap = new Map();
orderedMap.set('z', 1);
orderedMap.set('a', 2);
console.log([...orderedMap.keys()]); // ['z', 'a'] (insertion order)

// Use Object for static config
const config = {
  apiUrl: 'https://api.example.com',
  timeout: 5000
};

// Use Object when JSON serialization is needed
console.log(JSON.stringify(config));
// '{"apiUrl":"https://api.example.com","timeout":5000}'