JavaScript Closures Tutorial - Section 1: Fundamentals
To understand how closures work, we need to understand execution contexts. When JavaScript code runs, it creates execution contexts that contain:
thisJavaScript uses a call stack to manage execution contexts. Let's see it in action:
function first() {
console.log("In first");
second();
console.log("Back in first");
}
function second() {
console.log("In second");
third();
console.log("Back in second");
}
function third() {
console.log("In third");
}
first();
Each execution context has a lexical environment that consists of:
const globalVar = "global";
function outer() {
const outerVar = "outer";
function inner() {
const innerVar = "inner";
console.log(globalVar, outerVar, innerVar);
}
return inner;
}
const myFunc = outer();
myFunc();
When a function is created, it stores a reference to its lexical environment. This is how closures work:
function createCounter() {
let count = 0;
return function increment() {
count++;
return count;
};
}
const counter = createCounter();
Here's what happens step by step:
createCounter() is called, creating a new execution contextcount is created in this context's environmentincrement function is created and stores a reference to this environmentcreateCounter() returns and its execution context is popped off the stackincrement still references it!counter(), it accesses count through its stored referenceClosures keep their lexical environment alive in memory. This is powerful but requires understanding:
function createHeavyObject() {
const largeArray = new Array(1000000).fill("data");
return function() {
// This closure keeps largeArray in memory!
return largeArray.length;
};
}
const getLength = createHeavyObject();
// largeArray is still in memory even though createHeavyObject finished
JavaScript engines are smart - they only keep variables that are actually used by the closure:
function createClosure() {
const used = "I'm used";
const notUsed = "I'm not used";
const alsoNotUsed = "Me neither";
return function() {
console.log(used); // Only 'used' is captured
};
}
const myClosure = createClosure();
// Modern engines won't keep 'notUsed' and 'alsoNotUsed' in memory
When multiple functions are created in the same scope, they share the same lexical environment:
function createCounter() {
let count = 0;
return {
increment: function() {
count++;
return count;
},
decrement: function() {
count--;
return count;
},
getCount: function() {
return count;
}
};
}
const counter = createCounter();
counter.increment(); // count = 1
counter.increment(); // count = 2
counter.decrement(); // count = 1
console.log(counter.getCount()); // 1
// All three methods share the same 'count' variable!
Closures are efficient, but keep these points in mind:
// Creating many closures
const counters = [];
for (let i = 0; i < 1000; i++) {
counters.push(createCounter());
}
// Each counter has its own lexical environment in memory
You now understand the fundamentals of closures and how they work under the hood! In the next section, we'll explore practical patterns like data privacy, factory functions, and the module pattern.