JavaScript Closures & Scope Explained — Lexical Scoping, Hoisting & this Binding
JavaScript closures and scope are the foundation of how variables are accessed and preserved in JavaScript. A closure is a function that “remembers” the variables from its outer scope even after that outer function has finished running. The Doda Browser uses closures for its event handlers, callback functions, and module system. The Durga Antivirus Pro dashboard uses closures for debounced search inputs and private state in its scanner modules.
What You’ll Learn
- Lexical scoping — how JavaScript resolves variable names
- Closure patterns — module pattern, factory functions, partial application
- IIFE (Immediately Invoked Function Expression)
- Hoisting — how
var,let,const, and function declarations behave - The temporal dead zone (TDZ) for
letandconst thisbinding rules — default, implicit, explicit, arrow functions- Building a counter with closure
Why Closures and Scope Matter
Without closures, every function would start fresh with no memory of its context. Closures enable data privacy (private variables in modules), function factories (functions that create functions), and event-driven programming (callbacks that remember their environment). Scope rules determine what variables your code can access — mastering them prevents some of the most common JavaScript bugs.
Learning Path
flowchart LR
A[JS Functions & Scope] --> B[JS Objects & Prototypes]
B --> C[Closures & Scope Deep Dive]
C --> D[Design Patterns]
C --> E[Performance]
C --> F[You Are Here]
Lexical Scoping
JavaScript uses lexical (static) scoping — a variable’s scope is determined by where it’s declared in the source code, not where it’s called:
const global = 'I am global';
function outer() {
const outerVar = 'I am in outer';
function inner() {
const innerVar = 'I am in inner';
console.log(global); // 'I am global'
console.log(outerVar); // 'I am in outer'
console.log(innerVar); // 'I am in inner'
}
inner();
console.log(innerVar); // ReferenceError — not in scope
}
outer();How scope resolution works: When JavaScript encounters a variable, it looks up the scope chain — current function scope, then outer function scope, then global scope. This is why inner() can access outerVar but outer() cannot access innerVar.
What Is a Closure?
A closure is created when a function retains access to its outer lexical scope even after the outer function has returned:
function createCounter() {
let count = 0; // Private variable
return function() {
count++; // Inner function "closes over" count
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
How it works: When createCounter returns, its local variables would normally be garbage-collected. But the returned function holds a reference to count through the closure, so JavaScript keeps it alive. The counter function “remembers” its birth environment.
Analogy: A closure is like a backpack. The function leaves its birthplace carrying a backpack with all the variables it needs. Even after the birthplace is gone, the function still has its backpack.
Closure Patterns
Module Pattern (Private Variables)
function createUserStore() {
const users = []; // Private — can't be accessed from outside
return {
add(name) {
users.push({ name, id: Date.now() });
console.log(`${name} added. Total: ${users.length}`);
},
getAll() {
return [...users]; // Return a copy to prevent mutation
},
find(name) {
return users.find(u => u.name === name);
}
};
}
const store = createUserStore();
store.add('Alice'); // Alice added. Total: 1
store.add('Bob'); // Bob added. Total: 2
console.log(store.users); // undefined — private!
console.log(store.getAll().length); // 2
Factory Function
function createLogger(prefix) {
return function(message) {
console.log(`[${prefix}] ${message}`);
};
}
const infoLogger = createLogger('INFO');
const errorLogger = createLogger('ERROR');
infoLogger('Server started'); // [INFO] Server started
errorLogger('Connection failed'); // [ERROR] Connection failed
Partial Application
function multiply(a, b) {
return a * b;
}
function partial(fn, ...fixedArgs) {
return function(...remainingArgs) {
return fn(...fixedArgs, ...remainingArgs);
};
}
const double = partial(multiply, 2);
const triple = partial(multiply, 3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
IIFE (Immediately Invoked Function Expression)
An IIFE runs immediately and creates a private scope — useful before ES6 modules existed:
(function() {
const privateVar = 'Secret';
console.log('IIFE runs immediately');
})();
// privateVar is not accessible here
// With parameters
const result = (function(a, b) {
return a + b;
})(3, 4);
console.log(result); // 7
Modern use: IIFEs are rarely needed now that ES6 modules provide proper scoping, but you’ll still see them in legacy code and some build tools.
Hoisting
JavaScript moves declarations to the top of their scope during compilation:
// Function declarations are hoisted — this works
sayHello(); // 'Hello!'
function sayHello() {
console.log('Hello!');
}
// var is hoisted but initialized as undefined
console.log(myVar); // undefined (not ReferenceError!)
var myVar = 5;
console.log(myVar); // 5
// let/const are hoisted but NOT initialized (TDZ)
console.log(myLet); // ReferenceError: Cannot access before initialization
let myLet = 10;Temporal Dead Zone (TDZ)
The TDZ is the period between entering a scope and the variable’s declaration where let and const variables exist but cannot be accessed:
{
// TDZ starts here
// console.log(x); // ReferenceError!
let x = 5; // TDZ ends here
console.log(x); // 5
}
// Practical example
function test() {
console.log(typeof value); // undefined (TDZ doesn't apply to typeof...)
let value = 10;
}
test();Anticipating confusion: The TDZ exists because let and const don’t get initialized to undefined like var does. They exist in the scope but have no value until the declaration line is reached.
var vs let vs const
// var — function scoped
function example() {
if (true) {
var x = 10; // Scoped to function, not block
}
console.log(x); // 10 — accessible outside the block!
}
// let — block scoped
function example() {
if (true) {
let y = 20; // Scoped to this block only
}
console.log(y); // ReferenceError
}
// const — block scoped, cannot be reassigned
const PI = 3.14159;
// PI = 3; // TypeError: Assignment to constant variable
const obj = { name: 'Alice' };
obj.name = 'Bob'; // Allowed — const prevents reassignment, not mutation
| Feature | var | let | const |
|---|---|---|---|
| Scope | Function | Block | Block |
| Hoisted | Yes (undefined) | Yes (TDZ) | Yes (TDZ) |
| Reassignable | Yes | Yes | No |
| Redeclarable | Yes | No | No |
Best practice: Use const by default, let when you need reassignment, and never use var.
this Binding
The value of this depends on how a function is called, not where it’s defined:
// 1. Default binding — window (or undefined in strict mode)
function showThis() {
'use strict';
console.log(this); // undefined
}
// 2. Implicit binding — the object before the dot
const user = {
name: 'Alice',
greet() {
console.log(`Hello, ${this.name}`);
}
};
user.greet(); // 'Hello, Alice'
// 3. Explicit binding — call, apply, bind
function greet() {
console.log(`Hello, ${this.name}`);
}
const person = { name: 'Bob' };
greet.call(person); // 'Hello, Bob'
greet.apply(person); // 'Hello, Bob'
const boundGreet = greet.bind(person);
boundGreet(); // 'Hello, Bob'
// 4. Arrow functions — inherit this from enclosing scope
const obj = {
name: 'Charlie',
regular: function() {
console.log(this.name); // 'Charlie'
},
arrow: () => {
console.log(this.name); // undefined (this is window/global)
}
};The arrow function rule: Arrow functions don’t have their own this. They use this from the enclosing non-arrow function. This makes them ideal for callbacks:
class Timer {
constructor() {
this.seconds = 0;
// ❌ Regular function would lose `this`
// setInterval(function() { this.seconds++; }, 1000);
// ✅ Arrow function captures `this` from constructor
setInterval(() => { this.seconds++; }, 1000);
}
}Common Mistakes
1. Creating closures in loops (old-style)
// ❌ All callbacks log 3, not 0, 1, 2
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Output: 3, 3, 3
// ✅ Use let (block scoped)
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Output: 0, 1, 2
2. Confusing lexical scope with this binding
this is NOT determined by lexical scope — it’s determined by how the function is called. Arrow functions are the exception (they use lexical this).
3. Assuming var is block-scoped
if (true) {
var x = 5;
}
console.log(x); // 5 — not blocked!
Fix: Use let or const.
4. Forgetting that const doesn’t mean immutable
const arr = [1, 2, 3];
arr.push(4); // Allowed — arr is still the same reference
5. Losing this in callbacks
button.addEventListener('click', this.handleClick); // this is wrong
Fix: Use .bind(this), an arrow function, or store this as a variable.
Practice Questions
What is a closure? A function that retains access to its outer scope variables even after the outer function has returned.
What’s the difference between
var,let, andconst?varis function-scoped and hoisted withundefined.letandconstare block-scoped and have a temporal dead zone.constcannot be reassigned.How does
thisbehave differently in arrow functions? Arrow functions don’t have their ownthis— they inherit it from the enclosing non-arrow function.What is the temporal dead zone? The period between entering a scope and the variable’s declaration where
let/constvariables exist but can’t be accessed.
Challenge: Write a memoize(fn) function that uses closures to cache expensive function results, returning cached results for repeated calls with the same arguments.
Mini Project: Counter with Closure
function createAdvancedCounter(start = 0) {
let count = start;
const history = [];
return {
increment(step = 1) {
count += step;
history.push({ action: 'increment', step, timestamp: Date.now() });
return count;
},
decrement(step = 1) {
count -= step;
history.push({ action: 'decrement', step, timestamp: Date.now() });
return count;
},
reset() {
count = start;
history.push({ action: 'reset', timestamp: Date.now() });
return count;
},
getCount() {
return count;
},
getHistory() {
return [...history];
}
};
}
const counter = createAdvancedCounter(10);
console.log(counter.increment()); // 11
console.log(counter.increment(5)); // 16
console.log(counter.decrement(3)); // 13
console.log(counter.getCount()); // 13
console.log(counter.getHistory().length); // 3
FAQ
What’s Next
| Lesson | Description |
|---|---|
| JavaScript Home | Back to the JavaScript hub |
| https://tutorials.dodatech.com/programming-languages/javascript/js-functions/ | Functions & Scope basics |
| https://tutorials.dodatech.com/programming-languages/javascript/js-design-patterns/ | Design patterns in JavaScript |
| https://tutorials.dodatech.com/programming-languages/javascript/js-performance/ | Performance optimization |
| Node.js | Closures in Node.js async patterns |
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
What’s Next
Congratulations on completing this JavaScript Closures & Scope tutorial! Here’s where to go from here:
- Practice daily — Consistency is more important than long study sessions
- Build a project — Apply what you learned by building something real
- Explore related topics — Check out other tutorials in the same category
- Join the community — Discuss with other learners and share your progress
Remember: every expert was once a beginner. Keep coding!
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro