Scope, Closures & this
Scope
Scope determines where a variable is accessible. JavaScript has three levels.
Global, Function & Block Scope
const appName = "MyApp"; // global — accessible everywhere
function createUser() {
const name = "Alice"; // function scope — only inside createUser
if (true) {
let x = 10; // block scope — only inside this if
var y = 20; // function scope — leaks out of the block
}
// x is not accessible here. y is.
}
let and const are block-scoped. var is function-scoped and ignores blocks.
Lexical Scope
JavaScript uses lexical (static) scoping. A function accesses variables from where it is written in the source code, not where it is called.
const outer = "I am outer";
function parent() {
const inner = "I am inner";
function child() {
console.log(outer); // accessible — lexical parent chain
console.log(inner); // accessible — direct parent
}
child();
}
Closures
A closure is a function that remembers variables from its lexical scope, even after the outer function has returned.
function createCounter() {
let count = 0;
return function() {
count += 1;
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
The returned function closes over count. Even though createCounter has finished, the inner function still has access.
Data Privacy
Closures create private state in JavaScript.
function createBankAccount(initialBalance) {
let balance = initialBalance;
return {
deposit(amount) { balance += amount; return balance; },
withdraw(amount) {
if (amount > balance) throw new Error("Insufficient funds");
balance -= amount;
return balance;
},
getBalance() { return balance; }
};
}
const account = createBankAccount(100);
account.deposit(50); // 150
account.getBalance(); // 150
// account.balance is undefined — truly private
Factory Functions
function createLogger(prefix) {
return {
log(message) { console.log(`[${prefix}] ${message}`); },
error(message) { console.error(`[${prefix}] ERROR: ${message}`); }
};
}
const dbLogger = createLogger("DB");
dbLogger.log("Connected"); // [DB] Connected
Event Handlers
Closures let event handlers remember state without global variables.
function setupClickCounter(buttonId) {
let clicks = 0;
const button = document.querySelector(`#${buttonId}`);
button.addEventListener("click", () => {
clicks += 1;
button.textContent = `Clicked ${clicks} times`;
});
}
The this Keyword
this is determined by how a function is called, not where it is defined.
Default Binding
Standalone call: this is undefined in strict mode, globalThis otherwise.
"use strict";
function showThis() { console.log(this); }
showThis(); // undefined
Implicit Binding
Method call: this is the object before the dot.
const user = {
name: "Alice",
greet() { console.log(`Hello, I am ${this.name}`); }
};
user.greet(); // "Hello, I am Alice"
const greetFn = user.greet;
greetFn(); // this is lost — undefined in strict mode
Explicit Binding: call, apply & bind
function greet(greeting) {
console.log(`${greeting}, I am ${this.name}`);
}
const alice = { name: "Alice" };
greet.call(alice, "Hello"); // call: args individually
greet.apply(alice, ["Hi"]); // apply: args as array
const bound = greet.bind(alice); // bind: returns new function
bound("Hey");
bind is essential for callbacks: setTimeout(user.greet.bind(user), 100).
new Binding
new creates a fresh object and sets this to it.
function User(name) { this.name = name; }
const alice = new User("Alice");
console.log(alice.name); // "Alice"
Arrow Functions & Lexical this
Arrow functions have no this of their own. They inherit from the enclosing scope.
const team = {
name: "Engineering",
members: ["Alice", "Bob"],
listMembers() {
this.members.forEach(member => {
console.log(`${member} is on ${this.name}`); // works — arrow inherits this
});
}
};
A regular function inside forEach would lose this.
Priority Order (highest first)
newbinding- Explicit binding (
bind,call,apply) - Implicit binding (method call)
- Default binding (standalone call)
- Arrow functions always use lexical
this(cannot be overridden)
Common Pitfalls
Losing this in callbacks. Passing a method as a callback detaches it from its object. Fix with .bind() or an arrow function wrapper.
Using arrow functions as object methods. Arrow functions inherit this from the enclosing scope, not the object.
const user = {
name: "Alice",
greet: () => console.log(`Hello, ${this.name}`) // this is not user
};
Closures capture by reference, not value. The closure sees the variable's current value at run time.
let message = "hello";
const fn = () => console.log(message);
message = "goodbye";
fn(); // "goodbye"
The classic loop bug. var in a loop shares one variable across all iterations. Use let.
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0); // 3, 3, 3
}
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0); // 0, 1, 2
}
Key Takeaways
constandletare block-scoped.varis function-scoped.- Lexical scope means a function accesses variables from where it was written, not where it is called.
- A closure is a function plus its lexical environment. Closures enable private state, factories, and stateful callbacks.
thisis determined by the call site, not the definition. Four binding rules with a clear priority order.- Arrow functions inherit
thisfrom their enclosing scope, making them ideal for callbacks inside methods. - Use
bindto preservethiswhen passing methods as callbacks.