Closures & Lexical Scope

Medium100% Free~25 mins#closures#lexical-scope#settimeout#scope-chain#heap-retention
Key Learning Objectives
✓

Define a closure using ECMAScript internal slots ([[Environment]]) and lexical scoping rules.

✓

Understand why closures keep Environment Records alive in the heap after their outer function returns.

✓

Solve the classic asynchronous loop problem (var in setTimeout vs let per-iteration binding).

✓

Identify memory leak patterns caused by unintended closure retention.

The Interview Problem

What is printed to the console when the following timer function executes, and how does JavaScript closure scoping determine the printed numbers?

1function runTimers() {
2 for (var i = 0; i < 3; i++) {
3 setTimeout(function() {
4 console.log('var:', i);
5 }, 10);
6 }
7
8 for (let j = 0; j < 3; j++) {
9 setTimeout(function() {
10 console.log('let:', j);
11 }, 10);
12 }
13}
14
15runTimers();
Predict Console Output
Interactive Challenge

Select the option that matches what standard ECMAScript prints to the console:

var: 3 (x3), let: 0, 1, 2

var: 0, 1, 2, let: 0, 1, 2

var: 3 (x3), let: 3 (x3)

var: undefined (x3), let: 0, 1, 2

V8 Engine Execution Trace
Step 1 of 8 (Line 1)

runTimers starts. var i is hoisted to function scope with initial value undefined.

Call Stack (Top = Active)
Global
runTimers()
Lexical Scope / Bindings
i:undefined
Console Stream
> [empty]

Deep Technical Breakdown

Anatomy of a Closure in V8

A closure is the combination of a function bundled together with references to its surrounding state (the Lexical Environment).

  1. The [[Environment]] Internal Slot: Every function object created in JavaScript holds an internal pointer [[Environment]] to the Environment Record in which it was instantiated. Even after runTimers() returns and its stack frame is destroyed, any Environment Record referenced by an active closure is retained in the heap.
  2. Why var Fails in Loops: var is function-scoped. There is only ONE variable i for the entire runTimers function. All 3 callback functions point to the exact same memory address. By the time the event loop executes the timer callbacks, synchronous code has already finished, leaving i = 3.
  3. Why let Works: Per ECMAScript specification section 14.7.5.7, when a for loop uses let or const in its header, a brand new lexical environment record is created for each iteration. The value from the previous iteration is copied over, creating an isolated binding for every loop cycle.
Common Traps & Mistakes

Thinking closures are only formed when a function is returned from another function. ANY function that references variables from an outer scope creates a closure.

Unintentionally creating closures inside large collections or event listeners, preventing garbage collection of large DOM or data references.

Assuming an IIFE is still necessary in modern JavaScript. Block-scoped `let` solved the loop closure issue cleanly.

FAANG Follow-Up Probes
Probe #1

How did developers solve the loop closure trap prior to ES6 let (explain IIFE and function factories)?

Probe #2

How can you simulate private class fields using closures, and what are the memory trade-offs compared to `#privateFields`?

Probe #3

How do modern JavaScript engines optimize closures to only retain referenced variables instead of the entire outer environment?