Memoization
Introduction
Memoization caches the result of a function call so repeated calls with the same inputs can return instantly. It is one of the most common optimization patterns in JavaScript interviews because it turns repeated work into a cache lookup.
A complete answer also discusses key generation, purity, memory growth, invalidation, and whether arguments can be serialized safely.
Why This Matters
Memoization powers dynamic programming, derived selectors, expensive formatting, search suggestions, compiler caches, and API-layer deduplication. It tests both implementation skill and performance judgment.
Theory
What can be memoized
Memoization is safest for pure functions: the same arguments always produce the same result and the function has no important side effects. If the function depends on time, random values, mutable external state, or network data, caching may return stale results.
Map-based cache
A Map is a better default than a plain object because it avoids prototype collisions, has a clear has method, and preserves insertion order for LRU eviction. For primitive interview inputs, JSON.stringify(args) is a common key.
Serialization trade-offs
JSON.stringify is convenient but imperfect. It drops some values, cannot serialize functions or symbols, throws on cycles, and treats object property order as part of the key. Production caches often accept a custom resolver or use nested maps keyed by identity.
Cache growth
A memoized function can leak memory if the cache grows forever. Stronger versions expose clear, enforce maxSize, or use time-based expiration.
Visual Diagrams
call fn(2, 3) | serialize args -> [2,3] | cache has key? -- yes --> return cached result | no | compute original function | store result in Map | return result
Only cache misses run the expensive function.
Code Examples
Map-based memoize
The default key uses serialized arguments. A resolver can replace that for custom keys.
Playground
Press Run to execute the code and see output here.
Output Prediction
Predict the output #1
1function memoize(fn) {2 var cache = new Map();3 return function () {4 var args = Array.prototype.slice.call(arguments);5 var key = JSON.stringify(args);6 if (cache.has(key)) {7 console.log('hit ' + key);8 return cache.get(key);9 }10 console.log('miss ' + key);11 var result = fn.apply(this, args);12 cache.set(key, result);13 return result;14 };15}16 17var add = memoize(function (a, b) {18 console.log('compute');19 return a + b;20});21 22console.log(add(2, 3));23console.log(add(2, 3));24console.log(add(3, 2));Coding Exercises
Memoize with a maximum cache size
HardImplement memoize(fn, options) using a Map. Use JSON.stringify(args) by default, allow an optional resolver, and evict the least recently used entry when maxSize is exceeded.
Interview Questions
1When is memoization unsafe?
It is unsafe when the function is not pure: external mutable state, current time, random values, network data, or important side effects can make cached answers incorrect.
Follow-ups
- How would you prevent unbounded memory growth?
- How would you memoize object arguments by identity?
2Why use `Map` instead of a plain object for a cache?
Map avoids prototype-key collisions, has reliable key APIs, preserves insertion order for eviction, and communicates key-value cache semantics clearly.
Quiz
1. What is the biggest weakness of `JSON.stringify(args)` as a memoization key?
Summary
- Memoization caches function results by argument key.
- It works best for pure functions with stable inputs and outputs.
- A `Map` plus serialized arguments is the standard interview implementation.
- Production-grade caches need eviction, invalidation, and better key strategies.
Cheat Sheet
Pattern: key args → check cache → compute on miss → store result.
Default key: JSON.stringify(args) for primitive inputs.
Use Map: safe keys, insertion order, clear API.
Watch out: impure functions, cycles, object identity, unbounded growth.