This post explains a quiz originally shared as a LinkedIn poll.
🔹 The Question
let counter = 0;
const tracker = {
get id() {
return ++counter;
}
};
const cache = new Map();
function memoize(obj, key) {
if (!cache.has(obj)) {
cache.set(obj, { [key]: obj[key] });
}
return cache.get(obj)[key];
}
console.log(memoize(tracker, 'id'));
console.log(memoize(tracker, 'id'));
console.log(tracker.id);
Hint: When does the getter execute? Think about when property access happens and what gets stored in the cache.
🔹 Solution
Correct Answer: A) 1, 1, 2
The output will be:
112
đź§ How this works
This quiz demonstrates a critical production bug pattern: getters with side effects combined with memoization. The core issue is that the getter executes at a specific moment during caching, and subsequent calls return the cached value, not the result of re-executing the getter.
The memoization function creates a cache that stores the result of accessing the property, not a reference to the getter itself. Once cached, the getter never runs again for memoized calls—but direct property access still triggers it.
The key insight: The memoize function evaluates obj[key] exactly once (during cache creation), stores the resulting value, and returns that same value on subsequent calls. The getter's side effect (incrementing counter) only happens during that initial evaluation.
🔍 Line-by-line explanation
- Initial state:
let counter = 0;
const tracker = { get id() { return ++counter; } };
-
counterstarts at 0 -
tracker.idis a getter that pre-increments counter and returns the new value
- First memoize call:
console.log(memoize(tracker, 'id'));
- Check:
!cache.has(tracker)→true(cache is empty) - Execute:
cache.set(tracker, { [key]: obj[key] }) - When creating
{ id: obj['id'] }, it evaluatestracker.id -
Getter executes:
++counter→ counter becomes 1, returns 1 - Cache stores:
tracker → { id: 1 } - Return:
cache.get(tracker)['id']→1(from the cached object) - Output: 1
- Second memoize call:
console.log(memoize(tracker, 'id'));
- Check:
!cache.has(tracker)→false(tracker is already in cache) - Skip the cache.set block entirely
- Return:
cache.get(tracker)['id']→1(from the cached object) - Getter does NOT execute - we're reading from the cached plain object
- Output: 1
- Direct property access:
console.log(tracker.id);
- Direct access to
tracker.idbypasses the cache entirely -
Getter executes:
++counter→ counter becomes 2, returns 2 - Output: 2
🔹 Key Takeaways
Getters execute at property access time: When you access
obj[key]wherekeyis a getter, the getter function runs immediately and returns a value.Memoization captures values, not behavior: Caching
{ [key]: obj[key] }stores the result of the getter execution, not a reference to the getter itself.Cached objects are plain objects: The cached object
{ id: 1 }is a regular object with a regular property—it doesn't inherit the getter from the original object.Side effects run once during cache creation: Any side effects in the getter (incrementing counters, logging, API calls) only happen during the initial cache population.
Direct access bypasses cache: Accessing
tracker.iddirectly always executes the getter, regardless of what's in the cache.WeakMap doesn't prevent this issue: Using WeakMap vs Map doesn't change the behavior—the problem is caching the value instead of the getter behavior.
This is a common memoization anti-pattern: Memoizing property access without considering whether the property is a getter with side effects leads to subtle bugs that are hard to debug.
Top comments (0)