LearnFrenzy
Sign in Take a free test

Functions, Scope and Closures · Lesson 10 of 40

JavaScript Scope and Closures

5 min read Updated Nov 7, 2026 Saurabh Samir

Closures are the concept that separates JavaScript beginners from confident developers – and they appear in almost every JavaScript interview. The idea is simple once you see it: a function remembers the variables from the place where it was created, even after that place has finished running. To understand that, we start with scope.

In this lesson

  • Global, function and block scope
  • The scope chain and lexical scoping
  • What a closure is and why it works
  • Real uses: private state, once(), memoisation

Scope: where a variable is visible

const appName = "LearnFrenzy";           // module (top-level) scope

function showCourse() {
  const course = "JavaScript";            // function scope
  if (true) {
    const lesson = "Closures";            // block scope
    console.log(appName, course, lesson); // inner code sees outer variables
  }
  console.log(typeof lesson);             // block variable is gone
}
showCourse();
console.log(typeof course);               // function variable not visible outside

Output

LearnFrenzy JavaScript Closures
undefined
undefined

The scope chain

When code uses a variable, JavaScript looks in the current scope, then the enclosing scope, and so on outward until the global scope. If it's not found anywhere: ReferenceError.

Looking up lesson, course and appName from the innermost block

  1. Block scopelesson ✔ – found here
  2. showCourse() scopecourse ✔
  3. Module scopeappName ✔
  4. Global scopeconsole, Math, setTimeout… then ReferenceError

JavaScript uses lexical (static) scoping: what a function can see is decided by where it is written in the code, not where it is called from.

const level = "module";
function printLevel() { console.log(level); }
function run() {
  const level = "inside run";
  printLevel();        // looks where printLevel was written, not here
}
run();

Output

module

Closures

A closure is a function together with the variables from its surrounding scope. When an outer function returns an inner function, the inner function keeps those variables alive:

function makeCounter() {
  let count = 0;                 // private to this counter
  return function () {
    count++;
    return count;
  };
}
const nextTicket = makeCounter();
console.log(nextTicket(), nextTicket(), nextTicket());

const another = makeCounter();   // a fresh, independent count
console.log(another(), nextTicket());

Output

1 2 3
1 4

What happens in memory

  1. makeCounter() runscreates a scope with count = 0
  2. returns the inner functionwhich holds a reference to that scope
  3. makeCounter has finishedbut its scope isn't garbage-collected – it's still referenced
  4. nextTicket() is calledreads and updates the same count each time

Each call to makeCounter creates a new scope, so another has its own count. Nothing outside can reach count directly – the closure makes it truly private.

Closures in real code

Private state with a public API

function createWallet(initial) {
  let balance = initial;
  const history = [];
  return {
    pay(amount) {
      if (amount > balance) return "Insufficient balance";
      balance -= amount;
      history.push(-amount);
      return `Paid ₹${amount}, left ₹${balance}`;
    },
    topUp(amount) { balance += amount; history.push(amount); },
    get balance() { return balance; },
    get history() { return [...history]; },
  };
}
const wallet = createWallet(500);
console.log(wallet.pay(200));
console.log(wallet.pay(900));
wallet.topUp(100);
console.log(wallet.balance, wallet.history);
try {
  wallet.balance = 1000000;      // getter only – no way to set it
} catch (e) {
  console.log(e.name + ":", e.message);
}
console.log(wallet.balance);

Output

Paid ₹200, left ₹300
Insufficient balance
400 [ -200, 100 ]
TypeError: Cannot set property balance of #<Object> which has only a getter
400

The only way to change balance is through pay and topUp, which enforce the rules. In strict mode (every ES module) writing to a getter-only property throws; in old non-module scripts it would be silently ignored.

once() and function factories

function once(fn) {
  let done = false, result;
  return (...args) => {
    if (!done) { done = true; result = fn(...args); }
    return result;
  };
}
const initPayment = once(() => { console.log("Payment SDK loaded"); return "ready"; });
console.log(initPayment(), initPayment());

const multiplier = (factor) => (n) => n * factor;   // returns a configured function
const double = multiplier(2), toPaise = multiplier(100);
console.log(double(21), toPaise(4.5));

Output

Payment SDK loaded
ready ready
42 450

Memoisation: caching results

function memoize(fn) {
  const cache = new Map();
  return (n) => {
    if (cache.has(n)) return cache.get(n);
    const value = fn(n);
    cache.set(n, value);
    return value;
  };
}
let calls = 0;
const slowSquare = (n) => { calls++; return n * n; };
const fastSquare = memoize(slowSquare);
console.log(fastSquare(12), fastSquare(12), fastSquare(5));
console.log("real calls:", calls);

Output

144 144 25
real calls: 2

The classic interview question

for (var i = 1; i <= 3; i++) {
  setTimeout(() => console.log("var", i), 0);
}
for (let i = 1; i <= 3; i++) {
  setTimeout(() => console.log("let", i), 0);
}

Output

var 4
var 4
var 4
let 1
let 2
let 3

Interview angle

All three var callbacks close over the same function-scoped i, which is 4 when the timers fire. let creates a new binding per iteration, so each callback closes over its own value. Before let existed, the fix was an IIFE: ((j) => setTimeout(() => console.log(j)))(i).

Watch memory

A closure keeps its whole scope alive. Attaching closures that capture large data to long-lived things (global event listeners, caches that never shrink) can leak memory – remove listeners you no longer need.

Practice

Try it yourself

  1. Write makeRateLimiter(limit) returning a function that returns true for the first limit calls and false afterwards.

    Show solution
    function makeRateLimiter(limit) {
      let used = 0;
      return () => {
        if (used >= limit) return false;
        used++;
        return true;
      };
    }
    const canSendOtp = makeRateLimiter(2);
    console.log(canSendOtp(), canSendOtp(), canSendOtp());

    Output

    true true false

Quick check

Define a closure in one sentence.

A function bundled with references to the variables of the scope in which it was created, which it can still use after that scope has returned.

Is scope decided where a function is called or where it is written?

Where it is written – JavaScript is lexically scoped.

Two counters from makeCounter() – do they share count?

No – each call creates a new scope with its own count.

Summary

  • Variables live in global, module, function or block scope; lookups walk the scope chain outward.
  • Scope is lexical: decided by where code is written.
  • Closures keep outer variables alive – use them for private state, factories, once() and caching.

Finished this lesson?

Mark it complete to track your progress, then carry on with the next lesson.

Next lesson

Popular