Tutorials Logic, IN info@tutorialslogic.com

JavaScript Closures Lexical Scope,

JavaScript Closures Lexical Scope,

closures is an important JavaScript topic because it shows up in real projects, debugging sessions, and interviews. Learn the meaning first, then connect it to a small working example so the rule does not stay abstract.

Focus on what problem closures solves, where developers usually make mistakes, and how to verify the result with output, behavior, or a small test.

A strong understanding of closures should include syntax, behavior, one realistic use case, one failure case, and one quick way to check your work.

JavaScript Closures Lexical Scope should be studied as a practical JavaScript lesson, not as a label. Start by naming the input, the rule that changes the input, and the result a learner should be able to predict after reading the page.

In the javascript > closures page, the notes should connect the definition with a working scenario, a mistake that beginners actually make, and the exact check that proves the fix. That makes the topic useful for coding, debugging, and interview revision.

JavaScript Closures

A closure is created when a function remembers variables from the scope where it was created, even after that outer scope has finished running. In simple words, an inner function can keep access to variables from its parent function.

Closures are not a special syntax. They are a natural result of two JavaScript features working together: lexical scope and functions as values. Because functions can be returned, stored in variables, passed as callbacks, and executed later, they may carry their surrounding scope with them.

The Short Definition

A closure is a function bundled together with references to its outer lexical environment. That means the function can access variables that were in scope when the function was created.

When outer() finishes, its local execution is done. But inner() still needs message, so JavaScript keeps the required variable alive. That remembered access is the closure.

Smallest Closure Example

Smallest Closure Example
function outer() {
  const message = 'Hello from outer';

  function inner() {
    console.log(message);
  }

  return inner;
}

const savedFunction = outer();
savedFunction(); // Hello from outer

Lexical Scope Comes First

To understand closures, first understand lexical scope. Lexical scope means a variable's scope is decided by where the variable and function are written in the source code, not by where the function is called.

The inner() function can access topic because it is written inside outer(). The call location does not create the scope. The code location does.

Lexical Scope

Lexical Scope
const globalName = 'Tutorials Logic';

function outer() {
  const topic = 'Closures';

  function inner() {
    console.log(globalName); // available from global scope
    console.log(topic);      // available from outer scope
  }

  inner();
}

outer();

When Does a Closure Happen?

Every function has access to its outer scope while it runs. A closure becomes especially visible when a function continues to use that outer scope later. This usually happens when a function is returned, assigned to a variable, passed as a callback, or used by a timer or event listener.

  • A function is returned from another function.
  • A callback remembers variables from the surrounding function.
  • An event handler remembers state after setup code finishes.
  • A timer function runs later but still sees earlier variables.

Why Closures Matter

Closures are everywhere in JavaScript. They power private state, function factories, callbacks, event handlers, debounce and throttle utilities, memoization, module patterns, and many framework patterns.

  • Preserve values between function calls.
  • Create private variables without global variables.
  • Generate customized functions with factory functions.
  • Keep configuration available to callbacks and handlers.
  • Avoid repeating setup logic across many functions.

Private State with Closures

One of the most useful closure patterns is private state. Variables inside a function are not directly accessible from outside, but returned functions can still read and update them.

The count variable is private. Code outside createCounter() cannot read it directly, but the returned methods can access it because they close over the same lexical environment.

Private Counter

Private Counter
function createCounter() {
  let count = 0;

  return {
    increment() {
      count += 1;
      return count;
    },
    decrement() {
      count -= 1;
      return count;
    },
    getValue() {
      return count;
    }
  };
}

const counter = createCounter();

console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.getValue());  // 2
console.log(counter.count);       // undefined

Each Closure Gets Its Own State

Every time the outer function runs, JavaScript creates a new lexical environment. That means each closure can have its own independent state.

first and second were created by the same function, but they do not share the same count. Each call to createCounter() creates a fresh private variable.

Independent State

Independent State
function createCounter(start = 0) {
  let count = start;

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

const first = createCounter(0);
const second = createCounter(10);

console.log(first());  // 1
console.log(first());  // 2
console.log(second()); // 11
console.log(second()); // 12

Function Factories

A function factory creates and returns another function. Closures make factories powerful because the returned function remembers the configuration passed to the factory.

double() remembers multiplier = 2, while triple() remembers multiplier = 3. This is the same outer function producing different behavior.

Multiplier Factory

Multiplier Factory
function multiplyBy(multiplier) {
  return function(number) {
    return number * multiplier;
  };
}

const double = multiplyBy(2);
const triple = multiplyBy(3);

console.log(double(5)); // 10
console.log(triple(5)); // 15

Closures in Event Handlers

Event handlers often use closures because setup code runs once, while the handler function runs later. The handler can remember variables from setup time.

After setupButton() finishes, the click handler still remembers clicks and label. That is why every button can keep its own click count without using global variables.

Event Handler Closure

Event Handler Closure
function setupButton(button, label) {
  let clicks = 0;

  button.addEventListener('click', function() {
    clicks += 1;
    console.log(`${label} clicked ${clicks} times`);
  });
}

const saveButton = document.querySelector('#save');
setupButton(saveButton, 'Save');

Closures in Loops

Closures inside loops are a common source of confusion. With var, callbacks share one function-scoped variable. With let, each loop iteration gets a new block-scoped binding.

The var callbacks all see the same i, which has become 4 after the loop finishes. The let callbacks each close over a different j binding.

var vs let in Loops

var vs let in Loops
for (var i = 1; i <= 3; i++) {
  setTimeout(function() {
    console.log('var:', i);
  }, 100);
}

for (let j = 1; j <= 3; j++) {
  setTimeout(function() {
    console.log('let:', j);
  }, 100);
}

// Output:
// var: 4
// var: 4
// var: 4
// let: 1
// let: 2
// let: 3

Old Fix for var Loop Closures

Before let, developers often used an IIFE to create a new scope for each loop iteration. You will still see this pattern in old codebases and interview questions.

The IIFE receives the current value of i as currentValue. Each timeout closes over a different function call's variable.

IIFE Loop Fix

IIFE Loop Fix
for (var i = 1; i <= 3; i++) {
  (function(currentValue) {
    setTimeout(function() {
      console.log(currentValue);
    }, 100);
  })(i);
}

// Output:
// 1
// 2
// 3

Closures with Timers

Timers show closures clearly because the callback runs after the outer function has completed.

The callback passed to setTimeout() remembers message, even though remindUser() is no longer running when the timer fires.

Timer Closure

Timer Closure
function remindUser(name) {
  const message = `Hi ${name}, take a short break.`;

  setTimeout(function() {
    console.log(message);
  }, 2000);
}

remindUser('Asha');

Debounce with Closures

Debounce waits until repeated calls stop for a certain time. It is useful for search boxes, resize handlers, and input validation. Closures keep track of the timer ID between calls.

The returned function remembers timerId. Every call clears the previous timer and starts a new one.

Debounce Utility

Debounce Utility
function debounce(callback, delay) {
  let timerId;

  return function(...args) {
    clearTimeout(timerId);

    timerId = setTimeout(() => {
      callback.apply(this, args);
    }, delay);
  };
}

const search = debounce(function(query) {
  console.log('Searching for:', query);
}, 300);

search('j');
search('ja');
search('jav');
search('java'); // only this call runs after 300ms

Throttle with Closures

Throttle limits a function so it runs at most once during a time window. It is useful for scroll, mousemove, and resize events.

The returned function remembers waiting. While waiting is true, extra calls are ignored.

Throttle Utility

Throttle Utility
function throttle(callback, delay) {
  let waiting = false;

  return function(...args) {
    if (waiting) return;

    callback.apply(this, args);
    waiting = true;

    setTimeout(() => {
      waiting = false;
    }, delay);
  };
}

const onScroll = throttle(function() {
  console.log('Scroll handler ran');
}, 500);

window.addEventListener('scroll', onScroll);

Memoization with Closures

Memoization stores results so expensive work does not need to run again for the same input. A closure keeps the cache private.

The cache variable is not global. Only the returned function can access it.

Memoization

Memoization
function memoize(fn) {
  const cache = new Map();

  return function(value) {
    if (cache.has(value)) {
      console.log('From cache');
      return cache.get(value);
    }

    console.log('Calculated');
    const result = fn(value);
    cache.set(value, result);
    return result;
  };
}

const square = memoize(n => n * n);

console.log(square(5)); // Calculated, 25
console.log(square(5)); // From cache, 25

Module Pattern with Closures

Before ES modules became common, developers used closures to create modules with private variables and a public API. This pattern is still useful for understanding older JavaScript code.

Module Pattern

Module Pattern
const cartModule = (function() {
  const items = [];

  return {
    addItem(name, price) {
      items.push({ name, price });
    },
    getTotal() {
      return items.reduce((total, item) => total + item.price, 0);
    },
    getCount() {
      return items.length;
    }
  };
})();

cartModule.addItem('Keyboard', 50);
cartModule.addItem('Mouse', 20);

console.log(cartModule.getCount()); // 2
console.log(cartModule.getTotal()); // 70
console.log(cartModule.items);      // undefined

Closures and Async Code

Closures are common in asynchronous JavaScript because callbacks, promises, and async functions often run after surrounding code has moved on.

The returned loadUser() function remembers apiUrl. You do not need to pass the base URL every time.

Async Closure

Async Closure
function createUserLoader(apiUrl) {
  return async function loadUser(id) {
    const response = await fetch(`${apiUrl}/users/${id}`);
    return response.json();
  };
}

const loadUser = createUserLoader('https://api.example.com');

// loadUser remembers apiUrl.
loadUser(42).then(user => {
  console.log(user);
});

Closure Does Not Copy Values

A common misconception is that closures copy variable values. In reality, closures keep access to variables. If the variable changes, the closure sees the latest value.

log() does not store a frozen copy of 'First message'. It keeps access to the variable message, which was updated before the function was returned.

Reference to Variable

Reference to Variable
function createLogger() {
  let message = 'First message';

  function log() {
    console.log(message);
  }

  message = 'Updated message';
  return log;
}

const logger = createLogger();
logger(); // Updated message

Closures and Memory

Closures keep variables alive as long as the closure is reachable. This is useful, but it also means large objects can stay in memory if a closure still references them. In most apps this is not a problem, but it matters for long-lived event listeners, caches, and large data structures.

Remove long-lived event listeners when they are no longer needed, especially in single-page applications where pages and components can mount and unmount without a full reload.

Avoid Holding Large Data

Avoid Holding Large Data
function attachHandler(button, largeData) {
  function handleClick() {
    console.log('Clicked');
    // If largeData is referenced here, it stays alive.
  }

  button.addEventListener('click', handleClick);

  return function cleanup() {
    button.removeEventListener('click', handleClick);
  };
}

const cleanup = attachHandler(document.querySelector('button'), []);

// Later, when the handler is no longer needed:
cleanup();

Closures vs Classes

Closures and classes can both manage state. Closures are great for small private pieces of state and factory-style APIs. Classes are useful when you need many related methods, inheritance, or a familiar object-oriented structure.

Modern JavaScript classes support private fields with #, but closure-based privacy is still common and useful, especially for small utilities.

Closure vs Class

Closure vs Class
// Closure style
function createCounter() {
  let count = 0;
  return {
    increment: () => ++count,
    value: () => count
  };
}

// Class style
class Counter {
  #count = 0;

  increment() {
    return ++this.#count;
  }

  value() {
    return this.#count;
  }
}

Interview Explanation

If you are asked "What is a closure?" in an interview, give a short definition and then explain with a small example.

A strong interview answer: "A closure is when a function keeps access to variables from its outer lexical scope even after that outer function has returned. In this example, the returned function still remembers name."

Interview Answer

Interview Answer
function makeGreeting(name) {
  return function() {
    return `Hello, ${name}`;
  };
}

const greetAsha = makeGreeting('Asha');
console.log(greetAsha()); // Hello, Asha

Common Closure Use Cases

Use case What the closure remembers
Private counter The current count value
Function factory Configuration such as multiplier, prefix, or API URL
Event handler Setup state, DOM references, labels, or counters
Debounce The current timer ID
Throttle Whether the function is waiting
Memoization A private cache

Common Beginner Mistakes

  • Thinking closures copy values instead of keeping access to variables.
  • Forgetting that var is function-scoped in loop callbacks.
  • Creating hidden state when a simple parameter would be clearer.
  • Keeping large objects alive through long-lived callbacks.
  • Using closures for everything when a class, module, or plain object would be easier to read.

Summary

Closures happen when a function keeps access to variables from its outer lexical scope. They let JavaScript functions remember state, create private variables, produce customized functions, and support callbacks that run later. Once you understand lexical scope and function values, closures become less mysterious: a closure is simply a function carrying the variables it still needs.

JavaScript Closures Lexical Scope Java review example

JavaScript Closures Lexical Scope Java review example
class JavaScriptClosuresLexicalScopeReview {
    public static void main(String[] args) {
        String state = "ready";
        System.out.println("JavaScript Closures Lexical Scope: " + state);
    }
}

JavaScript Closures Lexical Scope guard example

JavaScript Closures Lexical Scope guard example
String value = null;
if (value == null) {
    System.out.println("JavaScript Closures Lexical Scope: handle the missing value before continuing");
}
Key Takeaways
  • Explain the purpose of closures before memorizing syntax.
  • Trace the exact call expression and confirm which value reached the parentheses.
  • Test one normal case, one edge case, and one mistake case for closures.
  • Write down why the value is not callable and what should hold the function instead.
  • Connect closures to a real project scenario instead of treating it as an isolated definition.
Common Mistakes to Avoid
WRONG Calling a value before checking whether it actually holds a function reference.
RIGHT Trace the variable assignment, the property lookup, and the actual call expression.
Most beginner errors come from skipping the behavior behind the syntax.
WRONG Memorizing JavaScript Closures Lexical Scope without the situation where it is useful.
RIGHT Connect JavaScript Closures Lexical Scope to a concrete JavaScript task.
Purpose makes syntax easier to recall.
WRONG Testing JavaScript Closures Lexical Scope only with the perfect input.
RIGHT Include empty, missing, duplicate, incompatible, or failed cases when relevant.
Real bugs usually appear outside the perfect path.
WRONG Memorizing JavaScript Closures Lexical Scope without the situation where it is useful.
RIGHT Connect JavaScript Closures Lexical Scope to a concrete JavaScript task.
Purpose makes syntax easier to recall.

Practice Tasks

  • Modify the example so it guards with `typeof` or uses the correct method name.
  • Write one mistake related to closures, then fix it and explain the fix.
  • Summarize when to use closures and when another approach is better.
  • Write a small example that uses JavaScript Closures Lexical Scope in a realistic JavaScript scenario.
  • Change one important value in the JavaScript Closures Lexical Scope example and predict the result first.

Frequently Asked Questions

A closure is a function that keeps access to variables from its outer lexical scope, even after the outer function has finished running.

No. Returning a function is the easiest example, but closures also appear in callbacks, event listeners, timers, promises, and any function that uses outer-scope variables later.

No. Closures keep access to variables, not frozen copies of values. If the variable changes, the closure can observe the changed value.

Closures reveal whether you understand lexical scope, async callbacks, private state, and common JavaScript patterns such as debounce, memoization, and function factories.

Ready to Level Up Your Skills?

Explore 500+ free tutorials across 20+ languages and frameworks.