Functions, Closures and this
Function declarations vs expressions vs arrow functions, closures, this binding, and prototypes.
Three ways to write a function
// Function declaration — hoisted, can be called before its definition appears
function add(a, b) {
return a + b;
}
// Function expression — not hoisted, assigned to a variable like any value
const subtract = function (a, b) {
return a - b;
};
// Arrow function — concise syntax, and (crucially) no own `this`
const multiply = (a, b) => a * b;
Function declarations are hoisted with their full body, so calling add() before its line in the file still works. Function expressions and arrow functions are not — the variable exists (if let/const) but isn't callable until execution reaches that line.
Arrow functions have a few syntax shortcuts worth knowing:
const square = n => n * n; // single param: parens optional
const greet = () => "Hello!"; // no params: parens required
const makePoint = (x, y) => ({ x, y }); // returning an object literal needs parens around it
Closures
A closure is a function that "remembers" the variables from the scope it was created in, even after that outer scope has finished executing. This is one of JavaScript's most powerful — and most commonly interview-tested — features.
function makeCounter() {
let count = 0; // private state, not accessible from outside
return function () {
count += 1;
return count;
};
}
const counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3 — `count` persisted between calls
makeCounter() returns, but the inner function still has access to count — it "closed over" that variable. Each call to makeCounter() creates a brand-new, independent count:
const counterA = makeCounter();
const counterB = makeCounter();
console.log(counterA()); // 1
console.log(counterA()); // 2
console.log(counterB()); // 1 — a separate closure, separate state
Closures are the mechanism behind private state in JavaScript (before class private fields existed), memoization caches, and event handler factories.
How this works — and why arrow functions are different
In a regular function, this is determined by how the function is called, not where it's defined:
const user = {
name: "Ada",
greet: function () {
console.log(`Hi, I'm ${this.name}`);
},
};
user.greet(); // "Hi, I'm Ada" — this === user, because it was called as user.greet()
const detached = user.greet;
detached(); // "Hi, I'm undefined" (or a TypeError in strict mode) — this is no longer user!
This is a classic source of bugs — passing a method as a callback loses its this:
class Timer {
constructor() {
this.seconds = 0;
}
tick() {
this.seconds += 1; // relies on `this` being the Timer instance
console.log(this.seconds);
}
}
const timer = new Timer();
// setInterval(timer.tick, 1000); // BUG: `this` inside tick() is undefined here
Arrow functions solve this by not having their own this at all — they inherit this lexically from the scope where they were defined, not from how they're called:
class Timer {
constructor() {
this.seconds = 0;
}
start() {
setInterval(() => {
this.seconds += 1; // `this` is inherited from start()'s `this` — the Timer instance
console.log(this.seconds);
}, 1000);
}
}
The other traditional fixes are .bind(this), or capturing const self = this; before the callback — both now largely superseded by just using an arrow function.
call, apply, and bind
These let you explicitly control what this refers to inside a regular function:
function introduce() {
return `Hi, I'm ${this.name}`;
}
const ada = { name: "Ada" };
console.log(introduce.call(ada)); // "Hi, I'm Ada" — calls immediately with this = ada
console.log(introduce.apply(ada)); // same as .call, but takes args as an array
const boundIntroduce = introduce.bind(ada); // returns a new function permanently bound to ada
console.log(boundIntroduce()); // "Hi, I'm Ada"
Prototypes, briefly
Every JavaScript object has an internal link to another object — its prototype — which it falls back to when a property lookup fails on the object itself. This prototype chain is how inheritance worked before (and still works underneath) the class syntax:
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function () {
return `${this.name} makes a sound`;
};
const dog = new Animal("Rex");
console.log(dog.speak()); // "Rex makes a sound" — found via the prototype chain
Modern class syntax is largely syntactic sugar over this same prototype mechanism:
class Animal {
constructor(name) {
this.name = name;
}
speak() {
return `${this.name} makes a sound`;
}
}
class Animal above still creates a constructor function under the hood, with speak attached to Animal.prototype — the class syntax is just far more readable than manually assigning to .prototype.
Common mistakes
- Passing an object method as a bare callback (
element.addEventListener("click", obj.method)) and losingthis— use an arrow function wrapper or.bind(). - Using a regular
functionfor a callback that needs the enclosingthis(e.g., inside a class method) instead of an arrow function. - Assuming closures capture a variable's value at creation time — they actually capture the variable itself, which matters a lot inside loops (see the classic
varloop closure bug, largely fixed by usingletinstead).
Interview questions
Q: What is a closure? Give a practical use case.
A closure is a function bundled together with references to the variables from its enclosing scope, which it retains access to even after that outer scope has returned. A practical use case is a counter or cache factory — makeCounter() returns a function that keeps its own private, persistent count variable no outside code can directly touch.
Q: Why do arrow functions behave differently with this?
A regular function's this is determined dynamically by its call site (obj.method() vs. a detached call). An arrow function has no this of its own at all — it lexically inherits this from the scope it was textually defined in, which is why arrow functions are the standard fix for losing this inside a callback passed out of a class method.