Memoization Maps and Sets

Introduction: From Clean Code to Efficient Code

In our previous lessons, we focused on the structure and safety of your code. You learned how closures allow functions to remember information and how composition lets you link small, pure functions together into pipelines. These patterns help you write code that is easy to read and maintain. However, as your applications grow, you also need to think about how your code performs when handling large amounts of data or complex calculations.

This lesson marks a shift from focusing only on how code looks to how efficiently it runs. We will explore two powerful built-in JavaScript structures: the Map and the Set. These tools will help us manage data more effectively and implement a professional performance pattern called memoization. By the end of this lesson, you will be able to store data for highly efficient access and ensure your functions never do the same hard work twice.

Understanding The Map Object

In JavaScript, we often use standard objects to store key-value pairs. While objects are useful, the Map object is specifically designed for high-performance data storage. One major advantage of a Map is that, unlike a regular object, a Map allows you to use any value as a key, including other objects or functions. It also remembers the order in which you added items, making it very predictable when you need to loop through your data.

Working with a Map involves a few core methods. You use set(key, value) to add data and get(key) to retrieve it. If you need to check whether a specific key already exists, use has(key). Additionally, you can instantly find out how many items are in your collection by checking the size property. These methods make your code more explicit and easier to read compared to the bracket notation used with standard objects.

What Is Memoization?

Memoization is a specific type of caching used to speed up computer programs. In functional programming, we know that pure functions return the same output every time they receive the same input. Because of this predictability, if a function performs a very "expensive" or time-consuming calculation, we can save the result of that calculation in a cache. The next time the function is called with the same input, we can simply return the saved result instead of running the calculation again.

This technique is especially helpful when dealing with recursive functions, which are functions that call themselves. Without memoization, a recursive function might calculate the same values thousands of times. By remembering previous results, we can turn a slow process that might take seconds into one that finishes significantly faster. It is a perfect example of using extra memory to save a significant amount of time.

Building A Memoize Helper

To implement this pattern, we can combine a Map with a closure. We can create a higher-order function called memoize that takes a function as an argument and returns a new, "smarter" version of that function. This new function keeps a private Map inside its closure to act as a cache.

"use strict";

const memoize = (fn) => {
  const cache = new Map();
  return (...args) => {
    const key = JSON.stringify(args);
    if (!cache.has(key)) {
      cache.set(key, fn(...args));
    }
    return cache.get(key);
  };
};

let calls = 0;
const fib = memoize((n) => {
  calls++;
  return n < 2 ? n : fib(n - 1) + fib(n - 2);
});

console.log("fib(20):", fib(20), "calls:", calls);

In this code, the memoize function uses JSON.stringify(args) to turn the function's arguments into a simple string key for JSON-serializable arguments. Inside the returned function, we check if the cache already has a result for that key. If it does not, we run the original function, store the result, and then return it. Note that while this works for basic data, production-grade memoization often requires a more robust strategy—such as a custom resolver or using WeakMap—to handle complex objects, functions, or circular references that JSON.stringify cannot process.

When we apply this to a Fibonacci function, the calls count remains very low because each number in the sequence is only calculated once. Notice that we use an anonymous arrow function for fib. This ensures that when the function calls itself, it calls the memoized version in the outer scope rather than an un-memoized internal name.

fib(20): 6765 calls: 21

The Set Object: Unique Values And Membership

Another essential tool for efficient data management is the Set. A Set is a collection of values where each value must be unique. If you try to add the same value twice, the Set simply ignores the second attempt. This makes it a perfect tool for "de-duplicating" data, such as finding the unique tags in a blog post or unique user IDs in a list of transactions.

const tags = ["premium", "verified", "premium", "vip", "verified"];
const unique = new Set(tags);

console.log("unique tags:", [...unique]);
console.log("has 'vip'?", unique.has("vip"));

In the example above, we pass an array with duplicate strings into the Set constructor. The Set automatically removes the duplicates. To turn the Set back into an array, we use the spread operator [...]. One of the biggest benefits of using a Set is the has() method. Unlike an array, where the computer might have to look through every single item to find a match, a Set is optimized to tell you whether a value exists very quickly, even as the collection grows.

unique tags: [ 'premium', 'verified', 'vip' ]
has 'vip'? true

Map As A Lookup Table

Beyond memoization, the Map object is excellent for creating lookup tables. This is a common pattern when you have an array of data and frequently need to find specific items by a unique identifier, like an ID number. Instead of using find() or a for loop every time — which can be slow if the array is large — you can convert the array into a Map once and then enjoy highly efficient lookups.

const transactions = [
  { id: 1, type: "deposit",  amount: 200 },
  { id: 2, type: "withdraw", amount: 50  },
  { id: 3, type: "deposit",  amount: 300 },
];

const byId = new Map(transactions.map((t) => [t.id, t]));

console.log("lookup #2:", byId.get(2));
console.log("size:", byId.size);

In this snippet, we transform our array of transactions objects into a Map. The Map constructor accepts an array of pairs, so we use map() to create a list of [id, object] pairs. Once this is done, calling byId.get(2) retrieves the transaction object directly without searching the entire list. This pattern is widely used in state management and data processing to keep applications feeling snappy and responsive.

lookup #2: { id: 2, type: 'withdraw', amount: 50 }
size: 3

Summary And Practice Preparation

In this lesson, we explored how to make our JavaScript applications more efficient by using Map and Set. We learned that Map is a robust tool for storing key-value pairs of any type and is the foundation for the memoization pattern. By caching the results of our functions using simple string keys, we can significantly improve performance. We also looked at how Set helps us maintain unique collections of data and perform fast membership checks.

As you move into the practice exercises, you will get hands-on experience building your own memoization helpers and managing data collections. Remember that the CodeSignal environment has everything you need pre-installed, so you can focus entirely on writing your logic. Pay close attention to how these structures simplify your code by removing the need for manual loops and complex conditional logic. Good luck!

Sign up

Join the 1M+ learners on CodeSignal

Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal