Tools: Closures in JavaScript: Understanding Lexical Scope and Memory

Tools: Closures in JavaScript: Understanding Lexical Scope and Memory

Source: Dev.to

The Problem Closures Solve: Functions Need Context ## How JavaScript Executes Functions: The Call Stack and Execution Contexts ## The Secret: Lexical Environments Live on the Heap ## Multiple Closures, Shared Environment ## Multiple Closures, Separate Environments ## The Loop Problem Revisited ## Practical Use Cases: Why Closures Matter ## 1. Data Privacy ## 2. Function Factories ## 3. Callbacks and Event Handlers ## 4. Memoization ## Memory Leaks: When Closures Go Wrong ## Closures and this: A Common Pitfall ## How Engines Optimize Closures ## Understanding Closures Through the Spec ## Summary: Closures are Lexical Scope + Memory If you've been writing JavaScript for a while, you've definitely used closures - even if you didn't know it. Every callback, every event handler, every setTimeout involves closures. But what exactly is a closure, and why does JavaScript work this way? The textbook definition is usually something like: "A closure is a function bundled together with references to its surrounding state (lexical environment)." But that doesn't explain the why or the how. Let's dive deep into what closures actually are at the engine level, why they exist, and what problems they solve. Here's a simple example: This works, but... how? When createCounter() finishes executing, shouldn't count be destroyed? The function has returned, its execution context is gone. So how does increment still have access to count? This is the fundamental question closures answer. Before we can understand closures, we need to understand how JavaScript runs code. When you call a function, JavaScript creates an Execution Context - a structure containing everything needed to run that function: These execution contexts are pushed onto the Call Stack. When a function returns, its execution context is popped off the stack. Call Stack during execution: When inner() returns, its context is popped: When outer() returns, its context is popped: So if execution contexts are destroyed when functions return, how do closures work? Here's the key insight: Execution Contexts live on the call stack (temporary), but Lexical Environments live on the heap (permanent, until garbage collected). When you create a function, that function gets a hidden internal property called [[Environment]]. This property stores a reference to the Lexical Environment where the function was created. Let's trace through our counter example: Step 1: Call createCounter() JavaScript creates an Execution Context for createCounter: Step 2: Create the increment function When JavaScript encounters function increment(), it creates a function object and sets its [[Environment]] property: The function object now has a permanent reference to Lexical Environment A. Step 3: Return from createCounter() The execution context for createCounter() is popped off the call stack. But Lexical Environment A stays in memory because the increment function still references it! Step 4: Call counter() When you call counter() (which is increment), JavaScript: To resolve count, JavaScript: The next time you call counter(), it creates a new Execution Context, but that context's outer reference still points to the same Lexical Environment A, where count is now 1. So it increments to 2. This is a closure: The increment function "closes over" the variables in Lexical Environment A, keeping them alive even after createCounter has returned. Here's where it gets interesting: All three methods share the same Lexical Environment: They all close over the same count variable because they were all created in the same scope. But if you call createCounter again, you get a completely independent closure: Each call to createCounter() creates a new Lexical Environment: Each counter is completely isolated. This is why closures are powerful for creating private state. Remember this classic problem? Now you can see exactly why this happens. Let's trace the Lexical Environments: All three arrow functions close over the same Global Lexical Environment. When they execute 100ms later, they all read i from that environment, where it's now 3. As we discussed in the var/let/const article, let in a loop creates a new Lexical Environment for each iteration: Each arrow function closes over a different environment with a different i. Before let existed, the solution was to create a new scope manually: The IIFE (Immediately Invoked Function Expression) creates a new Lexical Environment for each iteration, with its own j parameter. Each arrow function closes over its own j. JavaScript doesn't have private class fields (well, it does now with #, but closures were the original solution): There's no way to access balance except through the methods. It's truly private because it only exists in the Lexical Environment that the methods close over. Each returned function closes over its own multiplier value: Each click handler closes over its own message variable, even though setupButton has long since returned. The returned function closes over cache, which persists between calls. Closures keep Lexical Environments alive. This is usually good, but can cause memory leaks: Even though the click handler doesn't use data, the entire Lexical Environment (including data) is kept alive because the handler closes over it. Modern engines optimize this: V8 and other engines detect when variables aren't actually used in closures and don't keep them in memory. But it's still something to be aware of. Fix: Don't capture what you don't need: The problem: The function passed to setTimeout doesn't close over this. this is not a variable - it's determined by how a function is called, not where it's defined. Solution 1: Arrow functions (which don't have their own this): Solution 2: Capture this in a variable (the pre-ES6 way): Now self is a regular variable that the closure can capture. You might think: "Doesn't keeping all these Lexical Environments around use a lot of memory?" Modern JavaScript engines are very smart about closures: 1. Dead Variable Elimination: If a variable in a closure's environment is never used, the engine doesn't keep it in memory. The engine sees that notUsed is never accessed in the closure and doesn't keep it alive. 2. Sharing Environments: If multiple functions are created in the same scope and use the same variables, they can share an Environment Record. 3. Scope Analysis: During compilation, the engine figures out exactly which variables each function needs and creates optimized structures. The ECMAScript specification defines closures through these mechanisms: §9.1 Environment Records: The spec defines exactly how variables are stored and accessed. §9.4 Execution Contexts: Defines how contexts reference their outer environments. §10.2 Function Objects: Specifies that functions have a [[Environment]] internal slot. When you call a function: A closure isn't magic - it's a natural consequence of how JavaScript implements lexical scope: Understanding closures at this level helps you: Next time you write a callback or return a function from a function, you'll know exactly what's happening in memory - and why your variables are still accessible. Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse CODE_BLOCK: function createCounter() { let count = 0; return function increment() { count++; return count; }; } const counter = createCounter(); console.log(counter()); // 1 console.log(counter()); // 2 console.log(counter()); // 3 Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: function createCounter() { let count = 0; return function increment() { count++; return count; }; } const counter = createCounter(); console.log(counter()); // 1 console.log(counter()); // 2 console.log(counter()); // 3 CODE_BLOCK: function createCounter() { let count = 0; return function increment() { count++; return count; }; } const counter = createCounter(); console.log(counter()); // 1 console.log(counter()); // 2 console.log(counter()); // 3 CODE_BLOCK: function outer() { let x = 10; inner(); } function inner() { console.log('Hello'); } outer(); Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: function outer() { let x = 10; inner(); } function inner() { console.log('Hello'); } outer(); CODE_BLOCK: function outer() { let x = 10; inner(); } function inner() { console.log('Hello'); } outer(); CODE_BLOCK: 3. inner() execution context ← Currently executing 2. outer() execution context 1. Global execution context Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: 3. inner() execution context ← Currently executing 2. outer() execution context 1. Global execution context CODE_BLOCK: 3. inner() execution context ← Currently executing 2. outer() execution context 1. Global execution context CODE_BLOCK: 2. outer() execution context ← Back to executing outer 1. Global execution context Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: 2. outer() execution context ← Back to executing outer 1. Global execution context CODE_BLOCK: 2. outer() execution context ← Back to executing outer 1. Global execution context CODE_BLOCK: 1. Global execution context ← Back to global Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: 1. Global execution context ← Back to global CODE_BLOCK: 1. Global execution context ← Back to global CODE_BLOCK: function createCounter() { let count = 0; return function increment() { count++; return count; }; } const counter = createCounter(); Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: function createCounter() { let count = 0; return function increment() { count++; return count; }; } const counter = createCounter(); CODE_BLOCK: function createCounter() { let count = 0; return function increment() { count++; return count; }; } const counter = createCounter(); CODE_BLOCK: Call Stack: createCounter() execution context Global execution context Heap: Lexical Environment A { Environment Record: { count: 0 } Outer Reference: → Global Lexical Environment } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: Call Stack: createCounter() execution context Global execution context Heap: Lexical Environment A { Environment Record: { count: 0 } Outer Reference: → Global Lexical Environment } CODE_BLOCK: Call Stack: createCounter() execution context Global execution context Heap: Lexical Environment A { Environment Record: { count: 0 } Outer Reference: → Global Lexical Environment } CODE_BLOCK: Heap: Lexical Environment A { Environment Record: { count: 0 } Outer Reference: → Global } Function Object: increment [[Environment]]: → Lexical Environment A // This is the closure! Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: Heap: Lexical Environment A { Environment Record: { count: 0 } Outer Reference: → Global } Function Object: increment [[Environment]]: → Lexical Environment A // This is the closure! CODE_BLOCK: Heap: Lexical Environment A { Environment Record: { count: 0 } Outer Reference: → Global } Function Object: increment [[Environment]]: → Lexical Environment A // This is the closure! CODE_BLOCK: const counter = createCounter(); Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: const counter = createCounter(); CODE_BLOCK: const counter = createCounter(); CODE_BLOCK: Call Stack: Global execution context Heap: Lexical Environment A { Environment Record: { count: 0 } Outer Reference: → Global } Function Object: increment [[Environment]]: → Lexical Environment A Global Lexical Environment { Environment Record: { counter: → Function Object: increment } } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: Call Stack: Global execution context Heap: Lexical Environment A { Environment Record: { count: 0 } Outer Reference: → Global } Function Object: increment [[Environment]]: → Lexical Environment A Global Lexical Environment { Environment Record: { counter: → Function Object: increment } } CODE_BLOCK: Call Stack: Global execution context Heap: Lexical Environment A { Environment Record: { count: 0 } Outer Reference: → Global } Function Object: increment [[Environment]]: → Lexical Environment A Global Lexical Environment { Environment Record: { counter: → Function Object: increment } } CODE_BLOCK: Heap: Lexical Environment A { Environment Record: { count: 1 } // Updated! Outer Reference: → Global } Lexical Environment B (for this call to increment) { Environment Record: { } // No local variables Outer Reference: → Lexical Environment A } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: Heap: Lexical Environment A { Environment Record: { count: 1 } // Updated! Outer Reference: → Global } Lexical Environment B (for this call to increment) { Environment Record: { } // No local variables Outer Reference: → Lexical Environment A } CODE_BLOCK: Heap: Lexical Environment A { Environment Record: { count: 1 } // Updated! Outer Reference: → Global } Lexical Environment B (for this call to increment) { Environment Record: { } // No local variables Outer Reference: → Lexical Environment A } CODE_BLOCK: function createCounter() { let count = 0; return { increment() { count++; return count; }, decrement() { count--; return count; }, getCount() { return count; } }; } const counter = createCounter(); console.log(counter.increment()); // 1 console.log(counter.increment()); // 2 console.log(counter.decrement()); // 1 console.log(counter.getCount()); // 1 Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: function createCounter() { let count = 0; return { increment() { count++; return count; }, decrement() { count--; return count; }, getCount() { return count; } }; } const counter = createCounter(); console.log(counter.increment()); // 1 console.log(counter.increment()); // 2 console.log(counter.decrement()); // 1 console.log(counter.getCount()); // 1 CODE_BLOCK: function createCounter() { let count = 0; return { increment() { count++; return count; }, decrement() { count--; return count; }, getCount() { return count; } }; } const counter = createCounter(); console.log(counter.increment()); // 1 console.log(counter.increment()); // 2 console.log(counter.decrement()); // 1 console.log(counter.getCount()); // 1 CODE_BLOCK: Heap: Lexical Environment A { Environment Record: { count: 1 } } Function: increment [[Environment]]: → Lexical Environment A Function: decrement [[Environment]]: → Lexical Environment A Function: getCount [[Environment]]: → Lexical Environment A Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: Heap: Lexical Environment A { Environment Record: { count: 1 } } Function: increment [[Environment]]: → Lexical Environment A Function: decrement [[Environment]]: → Lexical Environment A Function: getCount [[Environment]]: → Lexical Environment A CODE_BLOCK: Heap: Lexical Environment A { Environment Record: { count: 1 } } Function: increment [[Environment]]: → Lexical Environment A Function: decrement [[Environment]]: → Lexical Environment A Function: getCount [[Environment]]: → Lexical Environment A CODE_BLOCK: const counter1 = createCounter(); const counter2 = createCounter(); console.log(counter1.increment()); // 1 console.log(counter2.increment()); // 1 console.log(counter1.increment()); // 2 console.log(counter2.getCount()); // 1 Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: const counter1 = createCounter(); const counter2 = createCounter(); console.log(counter1.increment()); // 1 console.log(counter2.increment()); // 1 console.log(counter1.increment()); // 2 console.log(counter2.getCount()); // 1 CODE_BLOCK: const counter1 = createCounter(); const counter2 = createCounter(); console.log(counter1.increment()); // 1 console.log(counter2.increment()); // 1 console.log(counter1.increment()); // 2 console.log(counter2.getCount()); // 1 CODE_BLOCK: Heap: Lexical Environment A { Environment Record: { count: 2 } } Lexical Environment B { Environment Record: { count: 1 } } counter1.increment [[Environment]]: → Lexical Environment A counter2.increment [[Environment]]: → Lexical Environment B Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: Heap: Lexical Environment A { Environment Record: { count: 2 } } Lexical Environment B { Environment Record: { count: 1 } } counter1.increment [[Environment]]: → Lexical Environment A counter2.increment [[Environment]]: → Lexical Environment B CODE_BLOCK: Heap: Lexical Environment A { Environment Record: { count: 2 } } Lexical Environment B { Environment Record: { count: 1 } } counter1.increment [[Environment]]: → Lexical Environment A counter2.increment [[Environment]]: → Lexical Environment B COMMAND_BLOCK: for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 100); } // Output: 3, 3, 3 Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 100); } // Output: 3, 3, 3 COMMAND_BLOCK: for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 100); } // Output: 3, 3, 3 CODE_BLOCK: Heap: Global Lexical Environment { Environment Record: { i: 0 } // Then 1, then 2, then 3 } Arrow Function 1 [[Environment]]: → Global Lexical Environment Arrow Function 2 [[Environment]]: → Global Lexical Environment Arrow Function 3 [[Environment]]: → Global Lexical Environment Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: Heap: Global Lexical Environment { Environment Record: { i: 0 } // Then 1, then 2, then 3 } Arrow Function 1 [[Environment]]: → Global Lexical Environment Arrow Function 2 [[Environment]]: → Global Lexical Environment Arrow Function 3 [[Environment]]: → Global Lexical Environment CODE_BLOCK: Heap: Global Lexical Environment { Environment Record: { i: 0 } // Then 1, then 2, then 3 } Arrow Function 1 [[Environment]]: → Global Lexical Environment Arrow Function 2 [[Environment]]: → Global Lexical Environment Arrow Function 3 [[Environment]]: → Global Lexical Environment COMMAND_BLOCK: for (let i = 0; i < 3; i++) { setTimeout(() => console.log(i), 100); } // Output: 0, 1, 2 Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: for (let i = 0; i < 3; i++) { setTimeout(() => console.log(i), 100); } // Output: 0, 1, 2 COMMAND_BLOCK: for (let i = 0; i < 3; i++) { setTimeout(() => console.log(i), 100); } // Output: 0, 1, 2 CODE_BLOCK: Heap: Lexical Environment (iteration 0) { Environment Record: { i: 0 } } Arrow Function 1 [[Environment]]: → Lexical Environment (iteration 0) Lexical Environment (iteration 1) { Environment Record: { i: 1 } } Arrow Function 2 [[Environment]]: → Lexical Environment (iteration 1) Lexical Environment (iteration 2) { Environment Record: { i: 2 } } Arrow Function 3 [[Environment]]: → Lexical Environment (iteration 2) Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: Heap: Lexical Environment (iteration 0) { Environment Record: { i: 0 } } Arrow Function 1 [[Environment]]: → Lexical Environment (iteration 0) Lexical Environment (iteration 1) { Environment Record: { i: 1 } } Arrow Function 2 [[Environment]]: → Lexical Environment (iteration 1) Lexical Environment (iteration 2) { Environment Record: { i: 2 } } Arrow Function 3 [[Environment]]: → Lexical Environment (iteration 2) CODE_BLOCK: Heap: Lexical Environment (iteration 0) { Environment Record: { i: 0 } } Arrow Function 1 [[Environment]]: → Lexical Environment (iteration 0) Lexical Environment (iteration 1) { Environment Record: { i: 1 } } Arrow Function 2 [[Environment]]: → Lexical Environment (iteration 1) Lexical Environment (iteration 2) { Environment Record: { i: 2 } } Arrow Function 3 [[Environment]]: → Lexical Environment (iteration 2) COMMAND_BLOCK: for (var i = 0; i < 3; i++) { (function(j) { setTimeout(() => console.log(j), 100); })(i); } // Output: 0, 1, 2 Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: for (var i = 0; i < 3; i++) { (function(j) { setTimeout(() => console.log(j), 100); })(i); } // Output: 0, 1, 2 COMMAND_BLOCK: for (var i = 0; i < 3; i++) { (function(j) { setTimeout(() => console.log(j), 100); })(i); } // Output: 0, 1, 2 COMMAND_BLOCK: function createBankAccount(initialBalance) { let balance = initialBalance; // Private variable return { deposit(amount) { if (amount > 0) { balance += amount; return balance; } }, withdraw(amount) { if (amount > 0 && amount <= balance) { balance -= amount; return balance; } }, getBalance() { return balance; } }; } const account = createBankAccount(100); account.deposit(50); console.log(account.getBalance()); // 150 console.log(account.balance); // undefined - can't access directly! Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: function createBankAccount(initialBalance) { let balance = initialBalance; // Private variable return { deposit(amount) { if (amount > 0) { balance += amount; return balance; } }, withdraw(amount) { if (amount > 0 && amount <= balance) { balance -= amount; return balance; } }, getBalance() { return balance; } }; } const account = createBankAccount(100); account.deposit(50); console.log(account.getBalance()); // 150 console.log(account.balance); // undefined - can't access directly! COMMAND_BLOCK: function createBankAccount(initialBalance) { let balance = initialBalance; // Private variable return { deposit(amount) { if (amount > 0) { balance += amount; return balance; } }, withdraw(amount) { if (amount > 0 && amount <= balance) { balance -= amount; return balance; } }, getBalance() { return balance; } }; } const account = createBankAccount(100); account.deposit(50); console.log(account.getBalance()); // 150 console.log(account.balance); // undefined - can't access directly! CODE_BLOCK: function createMultiplier(multiplier) { return function(number) { return number * multiplier; }; } const double = createMultiplier(2); const triple = createMultiplier(3); console.log(double(5)); // 10 console.log(triple(5)); // 15 Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: function createMultiplier(multiplier) { return function(number) { return number * multiplier; }; } const double = createMultiplier(2); const triple = createMultiplier(3); console.log(double(5)); // 10 console.log(triple(5)); // 15 CODE_BLOCK: function createMultiplier(multiplier) { return function(number) { return number * multiplier; }; } const double = createMultiplier(2); const triple = createMultiplier(3); console.log(double(5)); // 10 console.log(triple(5)); // 15 CODE_BLOCK: Heap: Lexical Environment A { multiplier: 2 } Lexical Environment B { multiplier: 3 } double [[Environment]]: → Lexical Environment A triple [[Environment]]: → Lexical Environment B Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: Heap: Lexical Environment A { multiplier: 2 } Lexical Environment B { multiplier: 3 } double [[Environment]]: → Lexical Environment A triple [[Environment]]: → Lexical Environment B CODE_BLOCK: Heap: Lexical Environment A { multiplier: 2 } Lexical Environment B { multiplier: 3 } double [[Environment]]: → Lexical Environment A triple [[Environment]]: → Lexical Environment B CODE_BLOCK: function setupButton(buttonId, message) { const button = document.getElementById(buttonId); button.addEventListener('click', function() { console.log(message); }); } setupButton('btn1', 'Button 1 clicked'); setupButton('btn2', 'Button 2 clicked'); Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: function setupButton(buttonId, message) { const button = document.getElementById(buttonId); button.addEventListener('click', function() { console.log(message); }); } setupButton('btn1', 'Button 1 clicked'); setupButton('btn2', 'Button 2 clicked'); CODE_BLOCK: function setupButton(buttonId, message) { const button = document.getElementById(buttonId); button.addEventListener('click', function() { console.log(message); }); } setupButton('btn1', 'Button 1 clicked'); setupButton('btn2', 'Button 2 clicked'); COMMAND_BLOCK: function memoize(fn) { const cache = {}; return function(...args) { const key = JSON.stringify(args); if (key in cache) { return cache[key]; } const result = fn(...args); cache[key] = result; return result; }; } const expensiveOperation = memoize((n) => { console.log('Computing...'); return n * n; }); console.log(expensiveOperation(5)); // Computing... 25 console.log(expensiveOperation(5)); // 25 (cached, no log) Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: function memoize(fn) { const cache = {}; return function(...args) { const key = JSON.stringify(args); if (key in cache) { return cache[key]; } const result = fn(...args); cache[key] = result; return result; }; } const expensiveOperation = memoize((n) => { console.log('Computing...'); return n * n; }); console.log(expensiveOperation(5)); // Computing... 25 console.log(expensiveOperation(5)); // 25 (cached, no log) COMMAND_BLOCK: function memoize(fn) { const cache = {}; return function(...args) { const key = JSON.stringify(args); if (key in cache) { return cache[key]; } const result = fn(...args); cache[key] = result; return result; }; } const expensiveOperation = memoize((n) => { console.log('Computing...'); return n * n; }); console.log(expensiveOperation(5)); // Computing... 25 console.log(expensiveOperation(5)); // 25 (cached, no log) CODE_BLOCK: function attachHandler() { const data = new Array(1000000).fill('leak'); document.getElementById('button').addEventListener('click', function() { console.log('Clicked'); }); } attachHandler(); Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: function attachHandler() { const data = new Array(1000000).fill('leak'); document.getElementById('button').addEventListener('click', function() { console.log('Clicked'); }); } attachHandler(); CODE_BLOCK: function attachHandler() { const data = new Array(1000000).fill('leak'); document.getElementById('button').addEventListener('click', function() { console.log('Clicked'); }); } attachHandler(); CODE_BLOCK: function attachHandler() { const data = new Array(1000000).fill('leak'); // Process data here if needed const result = processData(data); document.getElementById('button').addEventListener('click', function() { console.log('Clicked', result); // Only closes over result, not data }); } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: function attachHandler() { const data = new Array(1000000).fill('leak'); // Process data here if needed const result = processData(data); document.getElementById('button').addEventListener('click', function() { console.log('Clicked', result); // Only closes over result, not data }); } CODE_BLOCK: function attachHandler() { const data = new Array(1000000).fill('leak'); // Process data here if needed const result = processData(data); document.getElementById('button').addEventListener('click', function() { console.log('Clicked', result); // Only closes over result, not data }); } CODE_BLOCK: const obj = { name: 'Alice', greet() { setTimeout(function() { console.log('Hello, ' + this.name); }, 100); } }; obj.greet(); // Hello, undefined Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: const obj = { name: 'Alice', greet() { setTimeout(function() { console.log('Hello, ' + this.name); }, 100); } }; obj.greet(); // Hello, undefined CODE_BLOCK: const obj = { name: 'Alice', greet() { setTimeout(function() { console.log('Hello, ' + this.name); }, 100); } }; obj.greet(); // Hello, undefined COMMAND_BLOCK: const obj = { name: 'Alice', greet() { setTimeout(() => { console.log('Hello, ' + this.name); }, 100); } }; obj.greet(); // Hello, Alice Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: const obj = { name: 'Alice', greet() { setTimeout(() => { console.log('Hello, ' + this.name); }, 100); } }; obj.greet(); // Hello, Alice COMMAND_BLOCK: const obj = { name: 'Alice', greet() { setTimeout(() => { console.log('Hello, ' + this.name); }, 100); } }; obj.greet(); // Hello, Alice CODE_BLOCK: const obj = { name: 'Alice', greet() { const self = this; setTimeout(function() { console.log('Hello, ' + self.name); }, 100); } }; obj.greet(); // Hello, Alice Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: const obj = { name: 'Alice', greet() { const self = this; setTimeout(function() { console.log('Hello, ' + self.name); }, 100); } }; obj.greet(); // Hello, Alice CODE_BLOCK: const obj = { name: 'Alice', greet() { const self = this; setTimeout(function() { console.log('Hello, ' + self.name); }, 100); } }; obj.greet(); // Hello, Alice CODE_BLOCK: function outer() { let used = 1; let notUsed = new Array(1000000); return function() { return used; }; } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: function outer() { let used = 1; let notUsed = new Array(1000000); return function() { return used; }; } CODE_BLOCK: function outer() { let used = 1; let notUsed = new Array(1000000); return function() { return used; }; } - Variable Environment: Where local variables live - Lexical Environment: Where the function "looks up" variables (more on this soon) - this binding: What this points to - Outer Environment Reference: A link to the parent scope - Creates a new Execution Context for increment - Sets its Lexical Environment's outer reference to increment.[[Environment]] (Lexical Environment A) - Executes count++ - Looks in the current Environment Record (increment's local scope) - not found - Follows the outer reference to Lexical Environment A - found! count: 0 - Increments it to 1 - Create a new Function Environment Record - Set its outer environment reference to the function's [[Environment]] - This creates the closure - the function can now access variables from where it was defined - Functions store a reference to the Lexical Environment where they were created ([[Environment]]) - Lexical Environments live on the heap, not the call stack - When a function executes, it can access variables from its [[Environment]], even if the original execution context is gone - Multiple functions created in the same scope share the same Lexical Environment - Each call to an outer function creates a new, independent Lexical Environment - Private variables (data hiding) - Function factories (partial application) - Callbacks that remember context - Memoization and caching - Debug scope-related bugs - Avoid memory leaks - Write more efficient code - Understand React hooks, module patterns, and other advanced patterns