Closures & Lexical Scope
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 }78 for (let j = 0; j < 3; j++) {9 setTimeout(function() {10 console.log('let:', j);11 }, 10);12 }13}1415runTimers();
Predict Console Output
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.
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).
- 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 afterrunTimers()returns and its stack frame is destroyed, any Environment Record referenced by an active closure is retained in the heap. - Why
varFails in Loops:varis function-scoped. There is only ONE variableifor the entirerunTimersfunction. 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, leavingi = 3. - Why
letWorks: Per ECMAScript specification section 14.7.5.7, when aforloop usesletorconstin 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?
