Under The Hood
frontend javascript ES2015+ lexical scoping

Closures: Lexical Scope Environments & Heap Context Retention

Last updated
Prerequisites:
JavaScript functions and hoisting
Understanding of variables and block scope
  • closures
  • lexical-scope
  • heap
  • memory
  • scope-chain
  • environment

Read at your depth

The practical view

A closure is a function bundled with the variables that were in scope where it was defined. function outer() { let count = 0; return () => ++count; } const inc = outer(); — inc remembers count even after outer() returns, so inc() returns 1, then 2, etc. This works because the inner function holds a reference to the outer function's scope. Closures enable private state, factories, and callbacks that capture context. Be aware: every call to outer() creates a NEW closure with its own separate count, so two increments made from two different outer() calls do not share state.

The same idea in other frameworks

react equivalent
React hooks capture values per render; stale-closure bugs happen when a closure captures state from an old render's scope.

Legacy vs modern

Capturing a small value vs a large long-lived object in a closure

A closure keeps every captured variable alive for as long as the closure itself lives — so accidentally capturing heavy objects in long-lived closures pins memory.

before → after
Heavy capture in a long-lived closure
const big = loadHugeArray(); // ❌ OLD: captured by closure → pinned in heap
const fn = () => big[0]; // even if only one element is used
Extract only what the closure needs
const first = big[0]; // ✅ NEW: capture only the needed value
const fn = () => first; // big can now be garbage collected

Interview gotchas

Context

This tests the core mental model of closures: that captured variables are heap-allocated in a retained environment, not stack frames, and that this is the mechanism behind private state.

The mechanical answer

At compile time the engine identifies variables referenced by inner functions and context-allocates them in a heap-allocated environment (Context) object rather than a stack slot. When the outer function returns, its stack frame is popped, but the returned inner function holds an internal [[Environment]] reference to that Context. As long as any closure referencing it is reachable, the Context — and every variable slot in it — is retained by the garbage collector. That is exactly how the counter example works: the increment closure keeps the count slot alive in the heap across calls.

Trap

The naive answer is 'the variable stays in the function's stack frame'. Stack frames are popped and gone; captured variables must live in the heap. Another trap: assuming each call reuses one shared environment — every invocation of the outer function allocates its own Context, so separate calls produce independent, non-shared closures.

Under The Hood — a multi-depth technical interview hub.

Third-party licenses Press ⌘ K to search.