On this page

Closures, this, Hoisting, and Scope

How These Four Fit Together

These four ideas decide which variables a piece of code can see and what this points to:

ConceptAnswersIn one line
ScopeWhere can a variable be used?The region of code where a variable exists
HoistingCan I use it before the line that declares it?Declarations are set up before the code runs
ClosuresCan a function still use variables after their scope has ended?Yes, a function remembers where it was created
thisWhich object is this function working on?Decided by how the function is called (except arrow functions)

Scope and closures are about where code is written. this is about how code is called. Most interview confusion comes from mixing those two up.

Scope

Scope is the area of code where a variable can be used. JavaScript has three kinds:

ScopeCreated byVariables
Global / moduleThe file itselfTop-level variables of the file
FunctionEvery functionvar, let, const declared inside it
BlockAny { }: if, for, plain blocksOnly let and const

var ignores blocks; it belongs to the whole function. let and const stay inside the nearest { }.

function applyCoupon(order) {
if (order.total > 500) {
var discountVar = 50; // function-scoped
let discountLet = 50; // block-scoped
}
console.log(discountVar); // 50 (or undefined if the if didn't run)
console.log(discountLet); // ReferenceError: discountLet is not defined
}

Scope chain: when code uses a variable, JavaScript looks in the current scope, then the outer scope, then the next outer scope, up to the top. Inner code can read outer variables, never the reverse.

1module scope deliveryFee = 40
2 └── function placeOrder() total = 300
3 └── block { } tax = 15
4 → can see tax, total, deliveryFee

Because the chain depends on where the code is written, this is called lexical scope (lexical = based on the source code's layout).

In Node.js, each file has its own module scope. A top-level const in orders.js isn't visible in payments.js unless you export it.

Hoisting

Hoisting means JavaScript sets up all declarations in a scope before running any of its code. What you get if you use a variable early depends on how it was declared:

DeclarationHoisted?Using it before the declaration line
var xYes, set to undefinedundefined
let / constYes, but not initializedReferenceError
function foo() {}Yes, the whole functionWorks
var foo = function () {}Only var foo, as undefinedTypeError: foo is not a function
class Foo {}Yes, but not initializedReferenceError

The gap between the start of the scope and the let / const line is the temporal dead zone (TDZ): the variable exists but can't be touched yet.

console.log(fee); // undefined (var hoisted as undefined)
var fee = 40;
console.log(tax); // ReferenceError: Cannot access 'tax' before initialization
let tax = 15;
console.log(total(100)); // 140 (function declaration fully hoisted)
function total(amount) { return amount + 40; }
console.log(discount(100)); // TypeError: discount is not a function
var discount = function (amount) { return amount - 10; };

A classic trick question:

var status = 'placed';
function showStatus() {
console.log(status); // undefined, not 'placed'
var status = 'delivered'; // this var is hoisted to the top of showStatus
}
showStatus();

The inner var status is hoisted inside the function and shadows (hides) the outer one, so the log sees the inner variable before it gets a value.

Rule: use const by default, let when the value changes, and avoid var.

Closures

A closure is a function plus the variables it remembers from the scope where it was created. The function keeps access to those variables even after the outer function has returned.

Think of a delivery rider's bag. The rider leaves the restaurant, but still carries what was packed there. A closure carries the variables from where it was created.

function createOrderCounter(restaurantName) {
let count = 0; // private: no one outside can touch it
return function placeOrder() {
count++;
return restaurantName + ' order #' + count;
};
}
const biryaniHouse = createOrderCounter('Biryani House');
const pizzaPoint = createOrderCounter('Pizza Point');
console.log(biryaniHouse()); // Biryani House order #1
console.log(biryaniHouse()); // Biryani House order #2
console.log(pizzaPoint()); // Pizza Point order #1 (separate count)

createOrderCounter has finished running, but count lives on because placeOrder still uses it. Each call creates a new closure with its own count.

Where closures are used:

UseExample
Private statecount above can't be changed from outside
Function factoriescreateOrderCounter('Pizza Point') builds a ready-made function
CallbackssetTimeout, event handlers, and Express middleware remember outer variables
MemoizationCache results in an outer Map
OnceRun a setup function only once
// memoize: cache delivery estimates per route
function memoize(fn) {
const cache = new Map(); // remembered by the returned function
return async function (key) {
if (cache.has(key)) return cache.get(key);
const result = await fn(key);
cache.set(key, result);
return result;
};
}
const getEstimate = memoize(fetchEstimateFromMaps);
// Express middleware factory: role is remembered
function requireRole(role) {
return function (req, res, next) {
if (req.user.role !== role) return res.status(403).send('Forbidden');
next();
};
}
app.post('/restaurants/:id/menu', requireRole('owner'), updateMenu);

The loop question. Closures capture the variable, not a copy of its value:

for (var i = 0; i < 3; i++) {
setTimeout(function () { console.log('var:', i); }, 0);
}
// var: 3, var: 3, var: 3 -> one shared i, already 3 when the callbacks run
for (let j = 0; j < 3; j++) {
setTimeout(function () { console.log('let:', j); }, 0);
}
// let: 0, let: 1, let: 2 -> let creates a new j for every loop iteration

Memory note: whatever a closure references stays in memory as long as the closure exists. A long-lived closure, like a cache that never gets cleared or a listener that's never removed, is a common source of Node.js memory leaks.

this

this is the object a function is working on. For normal functions, it's decided when the function is called, not where it's written. Look at the call site:

How the function is calledthis is
order.getTotal() (method call)order, the object before the dot
getTotal() (plain call)undefined in strict mode (classes, ES modules); globalThis in sloppy-mode CommonJS
getTotal.call(order) / .apply(order) / .bind(order)order, set explicitly
new Order()The new object being created
Arrow functionNo own this: uses this from where the arrow was written

If several rules apply, the order is: new → bind / call / apply → method call → plain call. Arrow functions ignore all of them.

const restaurant = {
name: 'Biryani House',
getName: function () {
return this.name;
},
getNameArrow: () => {
return this.name; // arrow: 'this' from the module, not restaurant
},
getNameLater: function () {
setTimeout(() => {
console.log('later:', this.name); // arrow inside a method: uses getNameLater's this
}, 0);
}
};
console.log(restaurant.getName()); // Biryani House
console.log(restaurant.getNameArrow()); // undefined (top-level this in CommonJS is module.exports = {})
restaurant.getNameLater(); // later: Biryani House
const getName = restaurant.getName;
console.log(getName()); // undefined: plain call, 'this' lost

The most common bug: losing this in a callback. Passing obj.method passes only the function, not the object:

class OrderService {
constructor() {
this.pending = [];
}
addOrder(order) {
this.pending.push(order);
}
}
const service = new OrderService();
const orders = [{ id: 1 }, { id: 2 }];
// BUG: TypeError: Cannot read properties of undefined (reading 'pending')
orders.forEach(service.addOrder);
// FIX 1: bind 'this' permanently
orders.forEach(service.addOrder.bind(service));
// FIX 2: arrow function keeps the method call
orders.forEach((order) => service.addOrder(order));
// FIX 3: class field arrow, bound for every instance
class NotificationService {
constructor() { this.sent = 0; }
send = (userId) => { this.sent++; };
}

Classes always run in strict mode, so a lost this is undefined and throws right away, which makes the bug easy to spot.

call, apply, bind:

MethodRuns the function?Arguments
fn.call(obj, a, b)Yes, nowOne by one
fn.apply(obj, [a, b])Yes, nowAs an array
fn.bind(obj, a)No, returns a new function with this fixedPre-filled (optional)

Interview Angle

How to Explain it in an Interview

The 30-Second Spoken Pitch
"Scope decides where a variable is visible. var is function-scoped, while let and const are block-scoped, and inner code can read outer variables through the scope chain. Hoisting means declarations are set up before code runs: var starts as undefined, let and const exist but throw in the temporal dead zone, and function declarations are fully available. A closure is a function that remembers variables from where it was created, even after that outer function returns. I use closures for private state, factories like Express middleware that take options, and caching, and I keep in mind they hold memory as long as they live. this is different: for normal functions it depends on how the function is called. A method call gives the object before the dot, a plain call gives undefined in strict mode, call, apply and bind set it explicitly, and new creates a fresh object. Arrow functions don't have their own this, they use the surrounding one, which is why they're handy in callbacks but wrong as object methods."

Common mistakes:

  • Thinking let and const aren't hoisted. They are, but they're in the temporal dead zone.
  • Expecting a closure to copy a value. It keeps a live reference to the variable.
  • Using an arrow function as an object method and expecting this to be the object.
  • Passing service.method as a callback and losing this.

Summary

1Scope → var: function | let/const: block | inner sees outer (lexical)
2Hoisting → var = undefined | let/const = TDZ error | function decl = works
3 var fn = function → TypeError when called early
4Closure → function + remembered outer variables, live references
5 uses: private state, factories, middleware, memoize
6 loop: var shares one i (3,3,3) | let gets a new one (0,1,2)
7this → decided by the call: obj.fn() → obj | fn() → undefined (strict)
8 call/apply/bind → explicit | new → new object
9 arrow → no own this, uses the outer one
10Lost this → passing obj.fn as a callback → fix with bind or an arrow

this and new lead straight into how JavaScript objects share behaviour. Next, prototypes and classes explain what class really does under the hood.

Next TopicParallel vs Sequential Async Tasks, Limiting Concurrency