Skip to content

Intermediate · JavaScript in depth · topic 33 of 64 · 35 min

Scope, hoisting and closures

Explanation

Scope decides where a variable can be seen. let and const live only inside their { } block, and an inner function can see the variables of the code around it. A closure is a function that remembers those outer variables even after the outer function has finished. Closures power counters, once-only functions and private data.

Three kinds of scope

javascript
const site = 'Toolbox';        // global scope: seen everywhere in the file

function checkout() {
  const total = 500;           // function scope: only inside checkout
  if (total > 100) {
    const discount = 50;       // block scope: only inside this if
    console.log(site, total, discount); // all three are visible here
  }
  // console.log(discount);    // ReferenceError: discount is not defined
}
Inner code can see outer variables; outer code cannot see inner ones
Think of it like this: Scope is like rooms with one-way glass. From your bedroom (an inner block) you can see into the hall (the function around it) and out to the street (global scope). From the street, nobody can see into your bedroom.

let, const and the old var

Before 2015, JavaScript had only var. var ignores blocks (it belongs to the whole function), can be declared twice by mistake, and can be read before its line, giving undefined. These surprises cause bugs, so modern code uses const by default and let when the value must change. You will still see var in old code and old tutorials.

Hoisting and the temporal dead zone

Before running your code, JavaScript reads it once and notes every declaration. This is called hoisting. Function declarations are fully ready, so you can call them above the line where they are written. let and const are noted too, but they stay locked until their line runs. The time before that line is called the temporal dead zone, and using the variable there throws an error.

javascript
greet();                   // works: function declarations are hoisted
function greet() {
  console.log('Namaste');
}

console.log(city);         // ReferenceError: still in the temporal dead zone
const city = 'Jaipur';
Arrow functions stored in const follow the const rule: define them before you call them

Closures: functions that remember

A function can use variables from the place where it was created. If an outer function returns an inner function, the inner one keeps those variables alive, even though the outer function has finished. A function together with the variables it remembers is called a closure.

javascript
function makeCounter() {
  let count = 0;            // private to this counter
  return function () {
    count += 1;
    return count;
  };
}
const nextToken = makeCounter();
nextToken(); // 1
nextToken(); // 2
const other = makeCounter();
other();     // 1  (a separate count)
Each call of makeCounter creates a new, separate count

Useful closures: once and private data

javascript
function once(fn) {
  let done = false;
  let result;
  return (...args) => {
    if (!done) {
      done = true;
      result = fn(...args);
    }
    return result;
  };
}
const pay = once((amount) => `Paid Rs ${amount}`);
pay(500); // 'Paid Rs 500'
pay(500); // same answer, but the payment code ran only once

function makeWallet() {
  let balance = 0;           // no code outside can change this directly
  return {
    add(amount) { balance += amount; },
    getBalance() { return balance; },
  };
}
once stops double payments; makeWallet hides its balance

The classic loop bug

javascript
const fns = [];
for (var i = 0; i < 3; i++) {
  fns.push(() => i);
}
fns.map((f) => f()); // [3, 3, 3]  one shared i, already 3 when they run

const fixed = [];
for (let j = 0; j < 3; j++) {
  fixed.push(() => j);
}
fixed.map((f) => f()); // [0, 1, 2]  let makes a new j for every round
With var all the functions share one variable; with let each round gets its own

Higher-order and pure functions

A higher-order function takes a function as an argument or returns one: map, filter, once and makeCounter are all higher-order. A pure function always gives the same output for the same input and changes nothing outside itself, like (price) => Math.round(price * 1.18). Anything else a function does, such as changing an outer variable, updating the page or calling a server, is a side effect. Keep most functions pure and put side effects in a few clear places; pure functions are easy to test, as you do in every task in this course.

this in regular and arrow functions

javascript
const shop = {
  name: 'Sharma Kirana',
  regular() {
    return this.name;        // this = shop
  },
  arrow: () => this.name,    // an arrow has no own this; NOT shop here
  later() {
    setTimeout(() => console.log(this.name), 100); // borrows this from later: shop
  },
};
shop.regular(); // 'Sharma Kirana'
shop.later();   // prints 'Sharma Kirana' after 100 ms
Regular functions get this from the call; arrow functions borrow it from the code around them
Careful: Do not use arrow functions as object methods that need this. Do use arrow functions inside methods, for callbacks like setTimeout or map, because they borrow this from the method around them.

Watch the lesson

Scope, hoisting and closures · 1/7

global > function > block

Scope decides where a variable can be seen. Inner code sees outer variables, but not the other way round.

Prefer a video by a teacher? Find free videos on “javascript scope hoisting closures explained for beginners” on YouTube ↗

In real life: An OTP resend button

On a banking app, the "Resend OTP" button allows only three tries. The click handler is made by makeLimiter(3), which keeps a private count of attempts in a closure, so no other code on the page can reset it by mistake. The "Pay" button's handler is wrapped with once(), so a nervous double tap sends only one payment request.

Practice (0/3 done)

Task 1 · Code · Javascript

Write a function makeCounter() that returns a function. Each call of the returned function returns the next number: 1, then 2, then 3. Two counters must count separately. Example: const next = makeCounter(); next() is 1, then next() is 2.

Task 2 · Code · Javascript

Write a function once(fn) that returns a new function. The first time the new function is called, it calls fn with the same arguments and returns its result. Every later call does NOT call fn again; it returns the first result. Example: const pay = once((n) => n * 2); pay(5) is 10, and pay(7) is still 10.

Task 3 · Code · Javascript

makeHandlers(n) should return an array of n functions, where the function at index i returns i. So makeHandlers(3).map((f) => f()) should give [0, 1, 2], but the starter code gives [3, 3, 3]. Find the bug and fix it (one word changes).

Quick quiz

Question 1

1.What is a closure?

Question 2

2.if (true) { let x = 5; } console.log(x); What happens?

Question 3

3.Why can you call a function declaration above the line where it is written?

Question 4

4.Which of these is a pure function?

Question 5

5.A loop with var i pushes () => i three times. Calling them gives [3, 3, 3]. Why?

Read more (free)

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

← Previous: More array power