This content originally appeared on DEV Community and was authored by Priya Pandey
Have you ever met someone who asks the same question repeatedly even though they already got the answer? Yeah, don’t let your JavaScript functions be that person.
Memoization is a fancy saying: “Remember the answer so you don’t have to redo the work.” It’s one of those concepts that sounds simple but can go hilariously wrong if not done right.
In this article, we’ll explore:
- How memoization works (without boring you to death).
- The different ways to implement it (closures vs function properties).
- The biggest mistakes people make (like using bad keys). By the end, you’ll have a bulletproof memoization function that doesn’t make dumb mistakes. Let’s go!
So, why do we even need memorization?
Let’s say you have a super slow function — something like this:
function slowSquare(n) {
console.log(`Calculating square of ${n}... ⏳`);
// Simulating a slow computation
for (let i = 0; i < 1e9; i++) {}
return n * n;
}
console.log(slowSquare(5));
console.log(slowSquare(5));
Memoization Without Closures? Meet Function Properties!
So, we know closures work great for memoization, but did you know you can store cache directly on the function itself? Yep, functions in JavaScript are objects, meaning they can hold properties like an object does.
function memoizedSquare(n) {
if (!memoizedSquare.cache) {
memoizedSquare.cache = {}; // Initialize cache on first run
}
if (memoizedSquare.cache[n]) {
console.log("Memoized call!");
return memoizedSquare.cache[n];
}
console.log("New call!");
return (memoizedSquare.cache[n] = n * n);
}
console.log(memoizedSquare(4)); // New call!
console.log(memoizedSquare(4)); // Memoized call!
Why does this work?
- Every function in JavaScript is an object, so we can store a cache inside it.
- The cache is persistent across function calls, just like in closures.
- Less memory overhead compared to returning a whole new function.
Why you should avoid this?
- If you need to memoize multiple functions, they will each need their own cache property.
- Function properties can be accidentally modified or deleted, leading to unpredictable behavior.
- Another issue is this memory can’t be shared if you are extending the function if that is what you want. Let’s get deeper into that in next method.
Prototype Pollution: A Dangerous Mistake
Some developers might think, "Hey, let's store cache in the function prototype instead!" but this is a disaster waiting to happen.
function memoizedSquare(n) {
if (!memoizedSquare.prototype.cache) {
memoizedSquare.prototype.cache = {}; // Initialize cache on first run
}
if (memoizedSquare.prototype.cache[n]) {
console.log("Memoized call!");
return memoizedSquare.prototype.cache[n];
}
console.log("New call!");
return (memoizedSquare.prototype.cache[n] = n * n);
}
console.log(memoizedSquare(5)); // New call!
console.log(memoizedSquare(5)); // Memoized call!
Why is this very very bad?
- Pollutes all instances of the function — every function that uses the prototype will share the same cache by that I mean the functions that extend the above function will have access to the cache.
- Can be accidentally modified or overwritten by other parts of the code or even other functions extending them.
- Prototype pollution exploits exist, where malicious code can hijack properties stored in the prototype.
Moral of the story? Never store caches in the prototype! Use function properties only when necessary and be mindful of scope.
Now that we’ve covered this unusual method, let’s move on to the classic closures approach in the next section!
The Classic Approach: Closures for Memoization
If you’ve worked with memoization before, this is probably the method you’ve seen. Closures allow us to keep a cache inside a function without polluting the global scope or modifying function properties.
function square(val){
return val*val;
}
function memoizedFn(fn) {
let cache = new Map(); // using Map DS as its faster
return function(arg) {
if (cache.has(arg)) {
console.log("Memoized call!");
return cache.get(arg);
}
console.log("New call!");
const value = fn(arg);
return cache.set(arg,value);
return value;
};
}
const betterSquare = memoizedFn(square);
console.log(betterSquare(5)); // New call!
console.log(betterSquare(5)); // Memoized call!
Why Is This The Standard Approach?
- No global variables — cache is fully enclosed inside the function.
- Safe from modification — unlike function properties, this cache isn’t exposed.
- Ideal for multiple instances — you can create multiple memoized functions without interference.
Downsides of Closures
- Function Wrapping Required — you must return a new function every time.
- Memory Growth — The cache can grow indefinitely if not handled properly.
Bonus: Handling Key Generation
So far, we’ve only used numbers as keys, but what if the function takes other values like strings boolean?
function memoizedFn(fn) {
let cache = new Map();
return function(arg) {
if (cache.has(arg)) {
console.log("Memoized call!");
return cache.get(arg);
}
console.log("New call!");
const value = fn(arg);
return cache.set(arg,value);
return value;
};
}
const betterFn = memoizedFn(fn);
console.log(betterFn(1)); // New call!
console.log(betterFn('1')); // Memoized call!
Oh no! Those are two different values. You might have heard that before using a value as a key they converted it into strings. That’s exactly what happened here.
What is the solution? One of the solutions here is to generate key with the typeof that value. Something like this:
function generateKey(key){
return key+`${typeof key}`;
}
In the above case, our keys would look something like this:
cache = {
'1number': "something",
'1string': "something",
}
Hold it isn’t over. What if we have multiple arguments? We still have our protector rest operator and we will be using the apply method to give our function its context and our arguments.
function sum(a,b,c){
return a+b+c;
}
function generateKey(key){
const cacheKey=key.map(k=> (k+`${typeof k}`));
return cacheKey.join("_");
}
function memoizedFn(fn) {
let cache = new Map();
return function(...arg) {
const key=generateKey(arg);
if (cache.has(key)) {
console.log("Memoized call!");
return cache.get(key);
}
console.log("New call!");
const value = fn.apply(this, arg);
cache.set(key,value);
return value;
};
}
const betterSum = memoizedFn(sum);
console.log(betterSum(1,3,5)); // New call!
console.log(betterSum(1,3,5)); // Memoized call!
Memoization is a powerful technique that boosts performance by caching previously computed results. But the way you implement it depends on your use case!
The above memoization function can be very complex and can be modified to cover various use cases and edge cases. I have provided a basic generalized function to help you guys learn the concept. You can extend to generate your versions of memoized function and yes do share with me so that I can learn with you.
This content originally appeared on DEV Community and was authored by Priya Pandey

Priya Pandey | Sciencx (2025-02-23T13:59:09+00:00) Memoization in JavaScript: How Not to Be Dumb About It. Retrieved from https://www.scien.cx/2025/02/23/memoization-in-javascript-how-not-to-be-dumb-about-it/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.