Closures in JavaScript
A closure is a function's ability to "remember" and access variables from its lexical scope (the scope where the function was defined), even when the function is called outside that scope. Closures are one of the most powerful concepts in JavaScript — the foundation of many design patterns, private variables, and lots of modern features.
Lexical Scope
To understand closures, we first have to understand lexical scope.
// Global scope
const university = "Nusantara University";
function displayInfo() {
// Function scope — can access global variables
const faculty = "Engineering Faculty";
function displayDepartment() {
// Nested scope — can access both faculty AND university
console.log(`${university} - ${faculty}`);
}
displayDepartment();
}
displayInfo(); // "Nusantara University - Engineering Faculty"
// console.log(faculty); // ❌ Error! faculty does not exist in global scope
Code Example: What Is a Closure
// Closure: a function that "remembers" its lexical scope
function createCounter() {
let count = 0; // variable in the lexical scope
// This function closes over the 'count' variable
return function() {
count++;
console.log(`Count: ${count}`);
return count;
};
}
const counter = createCounter(); // counter is a closure
counter(); // Count: 1
counter(); // Count: 2
counter(); // Count: 3
// The 'count' variable cannot be accessed directly
// console.log(count); // ❌ ReferenceError
// Each closure has its own separate 'count'
const counter2 = createCounter();
counter2(); // Count: 1 (starts from 0 again!)
counter(); // Count: 4 (continues from before!)
// Closure with a parameter
function createGrader(gradeScale) {
// 'gradeScale' is closed over by the function below
return function(student) {
const grade = student.grade;
for (const [letter, threshold] of Object.entries(gradeScale)) {
if (grade >= threshold) return letter;
}
return "E";
};
}
const indonesiaGrading = createGrader({
"A": 85, "B": 70, "C": 55, "D": 40, "E": 0
});
const internationalGrading = createGrader({
"A": 90, "B": 80, "C": 70, "D": 60, "F": 0
});
const student1 = { name: "John", grade: 78 };
console.log(indonesiaGrading(student1)); // "B"
console.log(internationalGrading(student1)); // "C"
Practical Uses
// 1. ENCAPSULATION — Private variables
function academicSystem() {
// Private — cannot be accessed from the outside
let databaseStudent = [];
let databaseGrade = {};
let nextId = 1;
// Public interface — closure
return {
addStudent(name, studentId) {
const student = { id: nextId++, name, studentId };
databaseStudent.push(student);
databaseGrade[studentId] = [];
return student;
},
addGrade(studentId, course, grade) {
if (!databaseGrade[studentId]) return null;
databaseGrade[studentId].push({ course, grade });
return databaseGrade[studentId];
},
getAverage(studentId) {
const grade = databaseGrade[studentId];
if (!grade || grade.length === 0) return 0;
const total = grade.reduce((acc, n) => acc + n.grade, 0);
return total / grade.length;
},
getStudentCount() {
return databaseStudent.length;
}
};
}
const academicSystem = academicSystem();
const student1 = academicSystem.addStudent("John", "2023001");
academicSystem.addGrade("2023001", "Algorithms", 85);
academicSystem.addGrade("2023001", "Web Dev", 90);
console.log(academicSystem.getAverage("2023001")); // 87.5
console.log(academicSystem.getStudentCount()); // 1
// Cannot be accessed directly
// console.log(academicSystem.databaseStudent); // undefined
// 2. FUNCTION FACTORY — Creating similar functions with different behavior
function createValidator(pattern) {
// The closure captures the regex pattern
return function(input) {
return pattern.test(input);
};
}
const validationEmail = createValidator(/^[^@]+@[^@]+\.[^@]+$/);
const validationStudentID = createValidator(/^20\d{6}$/);
const validationPhone = createValidator(/^08\d{8,11}$/);
console.log(validationEmail("[email protected]")); // true
console.log(validationStudentID("20230001")); // true
console.log(validationPhone("081234567890")); // true
// 3. MEMOIZATION — Caching computation results
function createMemoize(fn) {
const cache = {}; // closure — remembered across calls
return function(...args) {
const key = JSON.stringify(args);
if (cache[key] !== undefined) {
console.log(`Cache hit: ${key}`);
return cache[key];
}
console.log(`Computing: ${key}`);
const result = fn(...args);
cache[key] = result;
return result;
};
}
const factorial = createMemoize(function(n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
});
console.log(factorial(10)); // Computing... → 3628800
console.log(factorial(10)); // Cache hit: [10] → 3628800
console.log(factorial(5)); // Cache hit: [5] → 120 (because it was already computed from 10!)
Code Explanation
| Concept | Explanation |
|---|---|
| Lexical Scope | A function accesses variables from where it was defined |
| Closure | A function + references to its lexical scope |
| Private variables | Variables that can only be accessed through the closure (encapsulation) |
| Function factory | A function that returns another function with specific behavior |
| Memoization | Caching results using a closure |
Why are closures important?
- JavaScript has no access modifiers (private, protected) — closures replace them.
- Closures make code more modular, reusable, and safe.
- Closures are used in Promises, event handlers, React hooks, and much more.
Real-World Examples
1. Counter for Tracking Access Counts
function createTracker(pageName) {
let visitCount = 0;
const visitLog = [];
return {
track() {
visitCount++;
const time = new Date().toLocaleTimeString("en-US");
visitLog.push(time);
return {
page: pageName,
total: visitCount,
time
};
},
getStatistics() {
return {
page: pageName,
totalVisits: visitCount,
firstVisit: visitLog[0] || "None yet",
lastVisit: visitLog[visitLog.length - 1] || "None yet"
};
}
};
}
const trackerHome = createTracker("Homepage");
const trackerProfile = createTracker("Profile");
trackerHome.track();
trackerHome.track();
trackerHome.track();
trackerProfile.track();
console.log(trackerHome.getStatistics());
// { page: "Homepage", totalVisits: 3, firstVisit: "...", lastVisit: "..." }
console.log(trackerProfile.getStatistics());
// { page: "Profile", totalVisits: 1, ... }
2. Academic System with Private Data
function createAcademicSystem() {
// Private data — can only be accessed through the closure
const dataStudent = new Map();
const facultyData = new Map();
let idCounter = 1000;
function generateId() {
idCounter++;
return `STU-${idCounter}`;
}
return {
registerStudent(name, studentId, majorCode) {
const id = generateId();
const student = {
id, name, studentId, majorCode,
registrationDate: new Date().toISOString(),
status: "Active"
};
dataStudent.set(id, student);
return student;
},
setStatusStudent(id, newStatus) {
const student = dataStudent.get(id);
if (!student) return { error: "Student not found" };
student.status = newStatus;
return student;
},
getAllStudents() {
return Array.from(dataStudent.values());
},
// Cannot access dataStudent directly from the outside!
};
}
const academicSystem = createAcademicSystem();
const student1 = academicSystem.registerStudent("John", "2023001", "TI");
const student2 = academicSystem.registerStudent("Amy", "2023002", "SI");
console.log(academicSystem.getAllStudents().length); // 2
// academicSystem.dataStudent = new Map(); // ❌ Cannot — it's private!
Memory Considerations
// ⚠️ A closure keeps references to its lexical scope
// If you're not careful, this can cause memory leaks
// ❌ Problematic — the closure holds onto large references
function processLarge() {
const largeData = new Array(1000000).fill("x");
return function() {
// Does not use largeData, but it is still referenced
return "done";
};
}
const fn = processLarge(); // largeData is still referenced in memory!
// ✅ Better — release the reference when it's no longer needed
function processBetter() {
let largeData = new Array(1000000).fill("x");
const result = processData(largeData);
largeData = null; // release the reference
return function() {
return result;
};
}
Conclusion
- A closure is a function + references to the lexical scope where it was defined.
- An inner function can access variables from the outer function even after the outer function has finished executing.
- Practical uses: encapsulation (private variables), function factories, memoization, counters, and many more.
- JavaScript has no
privatekeyword — closures replace it with patterns such as the revealing module. - Watch your memory: closures keep references to their lexical scope. Set variables to
nullwhen they are no longer needed. - Closures are used extensively in Promises, event handlers, React hooks, and other design patterns.