Immutability, WeakRefs & Memory Management

Shallow vs deep clone, Object.freeze, structuredClone, WeakMap/WeakSet for memory-safe caching, and how the garbage collector works.

deep hard โฑ 20 min immutabilityweakmapweaksetweakrefgarbage-collectionclonememory
Mastery:
Why interviewers ask this
Memory management and immutability are SDE-2 signals โ€” they show you think about the runtime, not just the API. WeakMap questions appear surprisingly often in senior screens.

Shallow vs deep clone

A shallow clone copies the top-level properties but nested objects remain shared references:

Run it yourself
Edit and run. Output is captured from console.log. Async logs (setTimeout/Promise) appear in real execution order.
Console
 

A deep clone recursively copies every level:

// structuredClone โ€” the modern standard (supported in Node 17+, all modern browsers)
const deep = structuredClone(original);
deep.address.city = 'LA';
console.log(original.address.city); // 'NY' โ€” untouched โœ“

// structuredClone handles: nested objects, arrays, Date, Map, Set, ArrayBuffer
// Does NOT support: functions, DOM nodes, class instances with methods, symbols

// For functions/class instances โ€” JSON roundtrip (lossy: undefined, Date โ†’ string)
const jsonClone = JSON.parse(JSON.stringify(obj));

// For complex class instances โ€” manual deep clone or libraries (lodash.cloneDeep)

Object.freeze

Object.freeze makes an objectโ€™s properties non-writable and non-configurable โ€” but only one level deep:

const config = Object.freeze({
  env: 'production',
  db: { host: 'localhost', port: 5432 }
});

config.env = 'dev';        // silently fails (throws in strict mode)
config.db.port = 9999;     // works! โ€” freeze is shallow

console.log(config.env);    // 'production' โœ“
console.log(config.db.port);// 9999 โ€” nested object was not frozen

// Deep freeze โ€” recursive
function deepFreeze(obj) {
  Object.freeze(obj);
  Object.keys(obj).forEach(key => {
    if (typeof obj[key] === 'object' && obj[key] !== null && !Object.isFrozen(obj[key])) {
      deepFreeze(obj[key]);
    }
  });
  return obj;
}

WeakMap โ€” memory-safe associative cache

Map holds strong references to its keys โ€” as long as the Map exists, the key objects canโ€™t be garbage collected, even if nothing else references them.

WeakMap holds weak references to keys โ€” if the key object has no other references, the GC can collect it and the WeakMap entry is automatically removed. Keys must be objects (not primitives).

// Use case: caching computed data associated with DOM nodes or objects
const cache = new WeakMap();

function getExpensiveMetrics(element) {
  if (cache.has(element)) return cache.get(element);
  const result = computeExpensiveMetrics(element);
  cache.set(element, result);
  return result;
}

// When `element` is removed from the DOM and no longer referenced elsewhere,
// the GC can collect it AND the WeakMap entry โ€” no memory leak.
// With a regular Map, element would be kept alive as long as cache existed.

WeakMap is not iterable โ€” you canโ€™t loop over it or get its size. This is intentional: the GC can remove entries at any time, so iteration would be non-deterministic.

Real uses: private class data, memoization tied to object lifecycle, metadata storage.

// Private data pattern (before private class fields existed)
const _private = new WeakMap();

class Circle {
  constructor(radius) {
    _private.set(this, { radius });
  }
  get area() {
    return Math.PI * _private.get(this).radius ** 2;
  }
}
// _private.get(circle) only accessible to code with a reference to _private

WeakSet

WeakSet is Set with weak references โ€” useful for tracking โ€œhas this object been processedโ€ without preventing GC:

const processed = new WeakSet();

function processOnce(obj) {
  if (processed.has(obj)) return; // already done
  doProcessing(obj);
  processed.add(obj);
  // When obj is GC'd, the WeakSet entry disappears automatically
}

WeakRef โ€” explicit weak references

WeakRef lets you hold a weak reference to an object and check if itโ€™s still alive:

class ExpensiveResource {
  compute() { return 'heavy result'; }
}

let resource = new ExpensiveResource();
const ref = new WeakRef(resource);

// Later...
resource = null; // remove strong reference

// GC may or may not have collected it yet
const obj = ref.deref();
if (obj) {
  obj.compute(); // still alive
} else {
  // obj was collected โ€” recreate if needed
}

Use WeakRef sparingly. Itโ€™s for caches where staleness is acceptable, not as a general reference mechanism.

Garbage collection basics

JavaScript uses mark-and-sweep GC. The GC starts from roots (global variables, call stack, registers) and marks everything reachable. Anything unmarked is garbage and gets collected.

Common memory leak patterns to recognize:

  1. Forgotten event listeners โ€” listeners on window/document that reference component state keep the component alive after unmounting. Always clean up in useEffect return function.
  2. Closures holding large references โ€” a closure captures the entire outer scope, including large objects.
  3. Detached DOM nodes โ€” a JS reference to a removed DOM node keeps the node (and its whole subtree) in memory.
  4. Timers โ€” setInterval without clearInterval keeps its callback (and everything it closes over) alive forever.
  5. Global caches without eviction โ€” a plain Map or {} used as a cache without size limits grows unbounded.

Say it out loud
โ€œShallow clone (spread/Object.assign) copies top-level props but nested objects remain shared โ€” mutations propagate. structuredClone does a spec-compliant deep clone. Object.freeze is shallow โ€” it protects top-level properties only. WeakMap uses weak object keys: when a key has no other strong references, the GC collects it and the entry disappears โ€” this prevents leaks in caches tied to object lifetimes. Unlike Map, WeakMap is not iterable because GC timing is non-deterministic. Common memory leaks: forgotten event listeners, non-cleared intervals, and global Maps without eviction.โ€

Likely follow-up questions
  • What's the difference between a shallow and deep clone?
  • When would you use WeakMap instead of Map?
  • Why can't you iterate a WeakMap?
  • What does Object.freeze do, and does it deep-freeze?
  • How does the garbage collector decide what to collect?

References