JavaScript Functions, Scope and Closures
AI-assistedDrafted with AI assistance and reviewed before publishing; examples are version-pinned. Verify against official docs when in doubt.
Functions are first-class in JavaScript: assign them to variables, pass them around, return them. Understanding them means understanding the scope chain and closures.
Three ways to define one
// 1. Declaration: hoisted, callable before its definition
function add(a, b) {
return a + b;
}
// 2. Expression: assigned to a variable, not hoisted
const multiply = function (a, b) {
return a * b;
};
// 3. Arrow function: shorter, no own this
const divide = (a, b) => a / b;
The differences that matter:
- Declarations are hoisted; expressions and arrows are not.
- Arrow functions inherit
thisfrom the surrounding scope; regular functions getthisfrom how they are called. - Use
functionwhen you need recursion by name, your ownthis, ornew; use arrows for callbacks and short transforms.
Arrow functions also have no arguments object and cannot be used as constructors.
Parameters
function greet(name = "anonymous", punctuation = "!") {
return `Hello, ${name}${punctuation}`;
}
greet(); // "Hello, anonymous!"
greet("ann"); // "Hello, ann!"
Defaults apply only when the argument is undefined:
greet(null); // "Hello, null!"
Rest parameters collect the remaining arguments into a real array:
function sum(...nums) {
return nums.reduce((total, n) => total + n, 0);
}
sum(1, 2, 3); // 6
The rest parameter must be last. arguments still exists in regular functions, but it is array-like and has no array methods; use ... in new code.
When a function takes several related options, accept an object and destructure it:
function createUser({ name, role = "user", active = true }) {
return { name, role, active };
}
createUser({ name: "ann", role: "admin" });
// { name: "ann", role: "admin", active: true }
The caller no longer has to remember parameter order.
Return values
A function without return returns undefined:
function log(msg) {
console.log(msg);
}
const result = log("hi"); // prints hi, result is undefined
Arrow functions returning an object literal need parentheses, otherwise the braces are read as a body:
const makeUser = (name) => ({ name, createdAt: Date.now() });
A return on its own line followed by a value is a classic bug:
function bad() {
return
{ ok: true }; // returns undefined; ASI inserted a semicolon
}
Scope and the scope chain
There are three levels: global, function, and block (let / const inside {}).
Lookups walk outward, which is what “scope chain” means:
const global = "global";
function outer() {
const mid = "mid";
function inner() {
const local = "local";
console.log(local, mid, global); // local mid global
}
inner();
}
Inner scopes can read outer variables; outer scopes cannot read inner ones:
function outer() {
const secret = "hidden";
}
console.log(secret); // ReferenceError: secret is not defined
Closures
A function keeps a reference to the scope where it was defined, even after that scope has returned:
function makeCounter() {
let count = 0;
return function () {
count += 1;
return count;
};
}
const next = makeCounter();
next(); // 1
next(); // 2
next(); // 3
count survives because the returned function still references it. Each call to makeCounter() creates an independent count, which is the basis of private state in JavaScript:
function requireMin(n) {
return (value) => value.length >= n;
}
const longEnough = requireMin(8);
["abc", "abcdefghij"].filter(longEnough); // ["abcdefghij"]
The var-in-a-loop trap
const fns = [];
for (var i = 0; i < 3; i++) {
fns.push(() => i);
}
fns.forEach((fn) => console.log(fn())); // 3 3 3
var is function scoped, so all three closures share one i, which is 3 when the loop ends.
With let, each iteration gets its own binding:
const fns = [];
for (let i = 0; i < 3; i++) {
fns.push(() => i);
}
fns.forEach((fn) => console.log(fn())); // 0 1 2
This is the most concrete reason to stop using var.
this in one paragraph
For a regular function, this is decided at call time:
const obj = {
value: 42,
getValue() {
return this.value;
},
};
obj.getValue(); // 42 — this is obj
const detached = obj.getValue;
detached(); // TypeError: this is undefined in strict mode
The method was extracted from the object, so there is no receiver. Arrow functions avoid the problem by inheriting this:
const timer = {
seconds: 0,
start() {
setInterval(() => {
this.seconds += 1; // still the timer object
}, 1000);
},
};
Use object method syntax when this should point at the object; use arrows for callbacks that should keep the enclosing this.
Common mistakes
1. Calling an arrow function before its definition
No hoisting; you get ReferenceError: Cannot access 'f' before initialization.
2. Expecting this to mean the caller in an arrow function
Arrows capture this lexically. A method written as an arrow will not see the object it is called on.
3. Returning an object literal without parentheses
() => { name } returns undefined; write () => ({ name }).
4. A return on its own line
Automatic semicolon insertion ends the statement; the value on the next line is unreachable.
5. Closures holding large objects
Anything referenced by a long-lived closure stays in memory. Do not capture a whole array when you need one field.
6. Async callbacks capturing a loop variable
With var they all see the final value; with let each iteration is independent.
7. Reassigning parameters
Mutating a parameter makes the function harder to reason about. Copy into a local const when you need a derived value.
Summary
functionfor hoisting,this, ornew; arrows for callbacks.- Defaults apply for
undefined; rest parameters collect the remainder into an array. - Closures keep their defining scope alive, which enables private state.
- Declare loop counters with
letto give each iteration its own binding. thiscomes from the call site for regular functions, from the enclosing scope for arrows.
Next: the array methods you will use every day.