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:
| Concept | Answers | In one line |
|---|---|---|
| Scope | Where can a variable be used? | The region of code where a variable exists |
| Hoisting | Can I use it before the line that declares it? | Declarations are set up before the code runs |
| Closures | Can a function still use variables after their scope has ended? | Yes, a function remembers where it was created |
this | Which 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:
| Scope | Created by | Variables |
|---|---|---|
| Global / module | The file itself | Top-level variables of the file |
| Function | Every function | var, let, const declared inside it |
| Block | Any { }: if, for, plain blocks | Only 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-scopedlet 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 = 402 └── function placeOrder() total = 3003 └── block { } tax = 154 → can see tax, total, deliveryFeeBecause 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:
| Declaration | Hoisted? | Using it before the declaration line |
|---|---|---|
var x | Yes, set to undefined | undefined |
let / const | Yes, but not initialized | ReferenceError |
function foo() {} | Yes, the whole function | Works |
var foo = function () {} | Only var foo, as undefined | TypeError: foo is not a function |
class Foo {} | Yes, but not initialized | ReferenceError |
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 initializationlet 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 functionvar 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 itreturn function placeOrder() {count++;return restaurantName + ' order #' + count;};}const biryaniHouse = createOrderCounter('Biryani House');const pizzaPoint = createOrderCounter('Pizza Point');console.log(biryaniHouse()); // Biryani House order #1console.log(biryaniHouse()); // Biryani House order #2console.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:
| Use | Example |
|---|---|
| Private state | count above can't be changed from outside |
| Function factories | createOrderCounter('Pizza Point') builds a ready-made function |
| Callbacks | setTimeout, event handlers, and Express middleware remember outer variables |
| Memoization | Cache results in an outer Map |
| Once | Run a setup function only once |
// memoize: cache delivery estimates per routefunction memoize(fn) {const cache = new Map(); // remembered by the returned functionreturn 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 rememberedfunction 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 runfor (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 called | this 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 function | No 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 Houseconsole.log(restaurant.getNameArrow()); // undefined (top-level this in CommonJS is module.exports = {})restaurant.getNameLater(); // later: Biryani Houseconst 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' permanentlyorders.forEach(service.addOrder.bind(service));// FIX 2: arrow function keeps the method callorders.forEach((order) => service.addOrder(order));// FIX 3: class field arrow, bound for every instanceclass 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:
| Method | Runs the function? | Arguments |
|---|---|---|
fn.call(obj, a, b) | Yes, now | One by one |
fn.apply(obj, [a, b]) | Yes, now | As an array |
fn.bind(obj, a) | No, returns a new function with this fixed | Pre-filled (optional) |
Interview Angle
How to Explain it in an Interview
Common mistakes:
- Thinking
letandconstaren'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
thisto be the object. - Passing
service.methodas a callback and losingthis.
Summary
1Scope → var: function | let/const: block | inner sees outer (lexical)2Hoisting → var = undefined | let/const = TDZ error | function decl = works3 var fn = function → TypeError when called early4Closure → function + remembered outer variables, live references5 uses: private state, factories, middleware, memoize6 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 object9 arrow → no own this, uses the outer one10Lost this → passing obj.fn as a callback → fix with bind or an arrowthis and new lead straight into how JavaScript objects share behaviour. Next, prototypes and classes explain what class really does under the hood.