Skillversitas

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 private keyword — closures replace it with patterns such as the revealing module.
  • Watch your memory: closures keep references to their lexical scope. Set variables to null when they are no longer needed.
  • Closures are used extensively in Promises, event handlers, React hooks, and other design patterns.