JavaScript
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax and Types
-
Section 3: Strings and Numbers in Depth
-
Section 4: Control Flow
-
Section 5: Functions
-
Section 6: Objects and Arrays
-
Section 7: Maps, Sets, and Symbols
-
Section 8: Asynchronous JavaScript
-
Section 9: Object-Oriented and Prototypes
-
Section 10: The DOM
-
Section 11: Browser APIs
-
Section 12: Modern JavaScript (ES2015-ES2025)
-
Section 13: Functional Programming Patterns
-
Section 14: Error Handling and Debugging
-
Section 15: Testing
-
Section 16: Accessibility for JavaScript Developers
-
Section 17: Internationalization and Localization
-
Section 18: Performance
-
Section 19: Node.js Fundamentals
-
Section 20: Regular Expressions
-
Section 21: Design Patterns in JavaScript
-
Section 22: Security Basics
-
Section 23: Data Structures and Algorithms in JavaScript
-
Section 24: Practical Projects
-
Section 25: More Advanced Async Patterns
-
Section 26: More Object and Class Practice
-
Section 27: Working with Dates and Internationalization
-
Section 28: Web Components
-
Section 29: More DOM and Browser Practice
-
Section 30: Build Tooling for Vanilla JavaScript
-
Section 31: More Practice Projects
-
Section 32: Interview and Algorithm Practice
-
Section 33: Error Objects (MDN Reference)
-
Section 34: TypedArrays and Binary Data
-
Section 35: Reflection and Metaprogramming (MDN Reference)
-
Section 36: More Global Functions (MDN Reference)
112: Memoization Techniques
Imagine you're a chef at a high-end restaurant. You have a signature sauce that takes four hours of simmering, reducing, and careful skimming to get just right. Now, you could start that process from scratch every single time a customer orders the dish, but you'd be out of business in a week—the customers would be waiting half a day for their dinner.
Instead, you make a massive batch in the morning and keep it in a chilled container in the fridge. When an order comes in, you don't go back to the stove; you check the fridge. If the sauce is there, you pour it out instantly. If you've run out, only then do you put in the grueling work to make more. That's memoization in a nutshell.
In JavaScript, we map this logic directly to our functions:
- The four-hour process is your "expensive" function (something that eats CPU or takes a long time).
- The fridge is a cache object or a
Mapstored in memory. - Checking the fridge is looking up the function's arguments as keys in that object.
- Pouring the sauce is returning the cached value without re-running the logic.
Building Your Own Cache Layer
I usually prefer building a generic memoization wrapper rather than hard-coding caches into every single function. It keeps the logic clean. Here is how I’d implement a basic version that can wrap any function.
const memoize = (fn) => {
const cache = new Map();
return (...args) => {
// We create a key based on the arguments
const key = JSON.stringify(args);
if (cache.has(key)) {
console.log('Fetching from cache...');
return cache.get(key);
}
console.log('Calculating result...');
const result = fn(...args);
cache.set(key, result);
return result;
};
};
// Let's simulate a "heavy" operation
const expensiveCalculation = (num) => {
let i = 0;
while (i < 1000000000) i++; // Artificial delay
return num * 2;
};
const memoizedCalc = memoize(expensiveCalculation);
console.log(memoizedCalc(10)); // Calculating result... 20
console.log(memoizedCalc(10)); // Fetching from cache... 20
Notice that I used JSON.stringify(args). Since the cache is a Map, and JavaScript compares objects by reference, not by value, we need a way to turn the arguments into a unique string key. It's a quick-and-dirty way to handle multiple arguments. Just be careful: if you're passing massive objects as arguments, stringifying them can actually become the new bottleneck.
When This Actually Becomes a Burden
Now, it's tempting to memoize everything, but I've seen developers shoot themselves in the foot by doing this. Memoization is a trade-off: you are trading memory for speed.
If your function is called with thousands of different arguments, your cache object will grow indefinitely, potentially leading to a memory leak. If the function is cheap to run—like adding two numbers—the overhead of checking the cache is actually slower than just doing the math. I only reach for this technique when the function is "pure" (meaning the same input always yields the same output) and the computation is noticeably heavy.
Handling Recursive Depth
The real magic happens with recursion. If you've ever tried to calculate a Fibonacci sequence without memoization, you know it hits a wall very quickly because it recalculates the same numbers millions of times. By memoizing the recursive call, you collapse an exponential time complexity into linear time. It's the difference between a browser tab freezing and getting an answer in 2 milliseconds.
📋 Practical Task
Building a Memoized Prime Factorization Engine
Your task is to create a function that finds the prime factors of a given number. Because calculating prime factors for very large numbers can be intensive, you need to implement a memoization layer to ensure that if the same number is checked twice, the result is returned instantly.
Requirements:
- Create a function
getPrimeFactors(n)that returns an array of prime factors for the numbern. - Wrap this function using a memoization technique (either a wrapper function or an internal cache).
- Test it by calling the function with a large number (e.g.,
123456789) twice. The first call should take a moment; the second call should be nearly instantaneous. - Log a message to the console whenever a result is retrieved from the cache versus when it is calculated from scratch.
There are no comments for now.