DEV Community

ValPetal Tech Labs
ValPetal Tech Labs

Posted on

Javascript Question of the Day #10 [Talk::Overflow]

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);
Enter fullscreen mode Exit fullscreen mode

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:

  • 1
  • 1
  • 2

đź§  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

  1. Initial state:
   let counter = 0;
   const tracker = { get id() { return ++counter; } };
Enter fullscreen mode Exit fullscreen mode
  • counter starts at 0
  • tracker.id is a getter that pre-increments counter and returns the new value
  1. First memoize call:
   console.log(memoize(tracker, 'id'));
Enter fullscreen mode Exit fullscreen mode
  • Check: !cache.has(tracker) → true (cache is empty)
  • Execute: cache.set(tracker, { [key]: obj[key] })
  • When creating { id: obj['id'] }, it evaluates tracker.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
  1. Second memoize call:
   console.log(memoize(tracker, 'id'));
Enter fullscreen mode Exit fullscreen mode
  • 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
  1. Direct property access:
   console.log(tracker.id);
Enter fullscreen mode Exit fullscreen mode
  • Direct access to tracker.id bypasses the cache entirely
  • Getter executes: ++counter → counter becomes 2, returns 2
  • Output: 2

🔹 Key Takeaways

  1. Getters execute at property access time: When you access obj[key] where key is a getter, the getter function runs immediately and returns a value.

  2. Memoization captures values, not behavior: Caching { [key]: obj[key] } stores the result of the getter execution, not a reference to the getter itself.

  3. 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.

  4. 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.

  5. Direct access bypasses cache: Accessing tracker.id directly always executes the getter, regardless of what's in the cache.

  6. 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.

  7. 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)