Shallow vs deep clone
A shallow clone copies the top-level properties but nested objects remain shared references:
console.log. Async logs (setTimeout/Promise) appear in real execution order.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:
- Forgotten event listeners โ listeners on
window/documentthat reference component state keep the component alive after unmounting. Always clean up inuseEffectreturn function. - Closures holding large references โ a closure captures the entire outer scope, including large objects.
- Detached DOM nodes โ a JS reference to a removed DOM node keeps the node (and its whole subtree) in memory.
- Timers โ
setIntervalwithoutclearIntervalkeeps its callback (and everything it closes over) alive forever. - Global caches without eviction โ a plain
Mapor{}used as a cache without size limits grows unbounded.
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.โ