🪟 Windows ServerSchnellladesäulen: Wenn der Ladestecker zickt - Golem.de(17.09.2026 um 23:29 Uhr)
🕵️ HackingPOL-MK: Betrug beim Online-Kauf - Presseportal(17.09.2026 um 17:57 Uhr)
🔧 ProgrammierungAgentic CLI customizations now in the usage metrics API(17.09.2026 um 23:08 Uhr)
🔧 ProgrammierungCopilot impact dashboard now shows feature engagement(17.09.2026 um 23:47 Uhr)
🪟 Windows ServerSchnellladesäulen: Wenn der Ladestecker zickt - Golem.de(17.09.2026 um 23:29 Uhr)
🕵️ HackingPOL-MK: Betrug beim Online-Kauf - Presseportal(17.09.2026 um 17:57 Uhr)
🔧 ProgrammierungAgentic CLI customizations now in the usage metrics API(17.09.2026 um 23:08 Uhr)
🔧 ProgrammierungCopilot impact dashboard now shows feature engagement(17.09.2026 um 23:47 Uhr)
🔧 Programmierung 🕛 vor 7 Monaten 10 Min Lesezeit
0

Closures in JavaScript: Understanding Lexical Scope and Memory

↗ Quelle (dev.to)
🗣️ Stimme:
📑 Inhaltsübersicht

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.






The Problem Closures Solve: Functions Need Context



Here's a simple example:




CODE
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






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.






How JavaScript Executes Functions: The Call Stack and Execution Contexts



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:





  • 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



These execution contexts are pushed onto the Call Stack. When a function returns, its execution context is popped off the stack.




CODE
function outer() {
let x = 10;
inner();
}

function inner() {
console.log('Hello');
}

outer();






Call Stack during execution:




CODE
3. inner() execution context  ← Currently executing
2. outer() execution context
1. Global execution context






When inner() returns, its context is popped:




CODE
2. outer() execution context  ← Back to executing outer
1. Global execution context






When outer() returns, its context is popped:




CODE
1. Global execution context  ← Back to global






So if execution contexts are destroyed when functions return, how do closures work?






The Secret: Lexical Environments Live on the Heap



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:




CODE
function createCounter() {
let count = 0;

return function increment() {
count++;
return count;
};
}

const counter = createCounter();






Step 1: Call createCounter()



JavaScript creates an Execution Context for createCounter:




CODE
Call Stack:
createCounter() execution context
Global execution context

Heap:
Lexical Environment A {
Environment Record: { count: 0 }
Outer Reference: → Global Lexical Environment
}






Step 2: Create the increment function



When JavaScript encounters function increment(), it creates a function object and sets its [[Environment]] property:




CODE
Heap:
Lexical Environment A {
Environment Record: { count: 0 }
Outer Reference: → Global
}

Function Object: increment
[[Environment]]: → Lexical Environment A // This is the closure!






The function object now has a permanent reference to Lexical Environment A.



Step 3: Return from createCounter()




CODE
const counter = 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!




CODE
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
}
}






Step 4: Call counter()



When you call counter() (which is increment), JavaScript:




  1. Creates a new Execution Context for increment

  2. Sets its Lexical Environment's outer reference to increment.[[Environment]] (Lexical Environment A)

  3. Executes count++



To resolve count, JavaScript:




  • 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

  • Returns 1




CODE
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
}






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.






Multiple Closures, Shared Environment



Here's where it gets interesting:




CODE
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






All three methods share the same Lexical Environment:




CODE
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






They all close over the same count variable because they were all created in the same scope.






Multiple Closures, Separate Environments



But if you call createCounter again, you get a completely independent closure:




CODE
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






Each call to createCounter() creates a new Lexical Environment:




CODE
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






Each counter is completely isolated. This is why closures are powerful for creating private state.






The Loop Problem Revisited



Remember this classic problem?




CODE
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Output: 3, 3, 3






Now you can see exactly why this happens. Let's trace the Lexical Environments:



During the loop:




CODE
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






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.



With let:




CODE
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Output: 0, 1, 2






As we discussed in the var/let/const article, let in a loop creates a new Lexical Environment for each iteration:




CODE
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)






Each arrow function closes over a different environment with a different i.



Before let existed, the solution was to create a new scope manually:




CODE
for (var i = 0; i < 3; i++) {
(function(j) {
setTimeout(() => console.log(j), 100);
})(i);
}
// Output: 0, 1, 2






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.






Practical Use Cases: Why Closures Matter






1. Data Privacy



JavaScript doesn't have private class fields (well, it does now with #, but closures were the original solution):




CODE
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!






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.






2. Function Factories






CODE
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






Each returned function closes over its own multiplier value:




CODE
Heap:
Lexical Environment A { multiplier: 2 }
Lexical Environment B { multiplier: 3 }

double [[Environment]]: → Lexical Environment A
triple [[Environment]]: → Lexical Environment B









3. Callbacks and Event Handlers






CODE
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');






Each click handler closes over its own message variable, even though setupButton has long since returned.






4. Memoization






CODE
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)






The returned function closes over cache, which persists between calls.






Memory Leaks: When Closures Go Wrong



Closures keep Lexical Environments alive. This is usually good, but can cause memory leaks:




CODE
function attachHandler() {
const data = new Array(1000000).fill('leak');

document.getElementById('button').addEventListener('click', function() {
console.log('Clicked');
});
}

attachHandler();






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:




CODE
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
});
}









Closures and this: A Common Pitfall






CODE
const obj = {
name: 'Alice',
greet() {
setTimeout(function() {
console.log('Hello, ' + this.name);
}, 100);
}
};

obj.greet(); // Hello, undefined






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):




CODE
const obj = {
name: 'Alice',
greet() {
setTimeout(() => {
console.log('Hello, ' + this.name);
}, 100);
}
};

obj.greet(); // Hello, Alice






Solution 2: Capture this in a variable (the pre-ES6 way):




CODE
const obj = {
name: 'Alice',
greet() {
const self = this;
setTimeout(function() {
console.log('Hello, ' + self.name);
}, 100);
}
};

obj.greet(); // Hello, Alice






Now self is a regular variable that the closure can capture.






How Engines Optimize Closures



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.




CODE
function outer() {
let used = 1;
let notUsed = new Array(1000000);

return function() {
return used;
};
}






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.






Understanding Closures Through the Spec



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:




  1. Create a new Function Environment Record

  2. Set its outer environment reference to the function's [[Environment]]

  3. This creates the closure - the function can now access variables from where it was defined






Summary: Closures are Lexical Scope + Memory



A closure isn't magic - it's a natural consequence of how JavaScript implements lexical scope:




  1. Functions store a reference to the Lexical Environment where they were created ([[Environment]])

  2. Lexical Environments live on the heap, not the call stack

  3. When a function executes, it can access variables from its [[Environment]], even if the original execution context is gone

  4. Multiple functions created in the same scope share the same Lexical Environment

  5. Each call to an outer function creates a new, independent Lexical Environment



Closures enable:




  • Private variables (data hiding)

  • Function factories (partial application)

  • Callbacks that remember context

  • Memoization and caching



Understanding closures at this level helps you:




  • Debug scope-related bugs

  • Avoid memory leaks

  • Write more efficient code

  • Understand React hooks, module patterns, and other advanced patterns



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.

Vollständiger Original-Artikel
Den kompletten Beitrag mit allen Details direkt auf dev.to lesen.
↗ Original-Artikel auf dev.to lesen
Wie bewertest du diesen Beitrag?
1 Klick Feedback
Teilen mit Netzwerk & Team:

Community-Analysen & Experten-Meinungen 0

Verfasse deine eigene Analyse, teile Workarounds oder diskutiere diesen Vorfall im Blog.
Noch keine Community-Analyse verfasst. Markiere einen Textabschnitt oder klicke oben auf Eigene Analyse verfassen“!
Community Pulse: Relevanz-Einschätzung
1 Klick Experten-Votum
🔴 Akute Relevanz 0%
🟡 In Evaluierung 0%
🟢 Keine Auswirkung 0%
Spannende Innovation 0%
Verwandte Story-Cluster & Quellen (Vektor-KI)
Port 8095 Engine
1 Quelle
Small AI models let drones autonomously identify and attack battlefield targets
1 Quelle
Here’s What the AI Apocalypse Could Look Like
1 Quelle
The FAA’s plan to fix air traffic? $875 million worth of AI
Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten Closures in JavaScript: Understanding Lexical Scope and Memory

Thematisch verwandte Begriffe: Closures, JavaScript, Understanding, Lexical · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...