JavaScript Functions, Scope, and Closures
- Description: Function declarations vs expressions vs arrows, parameters and defaults, rest/spread, scope rules, hoisting, and closures — including why every list/loop trap eventually leads here.
- My Notion Note ID: K2A-F4-4
- Created: 2018-03-23
- Updated: 2026-05-17
- License: Reuse is very welcome. Please credit Yu Zhang and link back to the original on yuzhang.io
Table of Contents
- 1. Three Ways to Define a Function
- 2. Parameters
- 3. Calling Functions
- 4.
thisBinding - 5. Scope
- 6. Hoisting
- 7. Closures
- 8. Higher-Order Functions
- 9. References
1. Three Ways to Define a Function
// Declaration — hoisted, has its own this/arguments
function greet(name) { return `Hi ${name}`; }
// Expression — assigned to a variable, anonymous or named
const greet = function (name) { return `Hi ${name}`; };
const greet = function greet(name) { return `Hi ${name}`; }; // named for stack traces
// Arrow — lexical this, no arguments, no super, can't be constructor
const greet = (name) => `Hi ${name}`;
const greet = name => `Hi ${name}`; // one param, no parens
const compute = (a, b) => {
const sum = a + b;
return sum * 2; // multi-line: braces + return
};
const make = () => ({ name: 'x' }); // returning object literal needs parens
Differences worth remembering:
| Feature | Declaration | Expression | Arrow |
|---|---|---|---|
| Hoisted | Yes (whole function) | Just the variable binding | Just the variable |
Has own this |
Yes | Yes | No — inherited from enclosing scope |
Has own arguments |
Yes | Yes | No |
Can be new'd |
Yes | Yes | No (TypeError) |
| Implicit return | No | No | Yes if no body braces |
.name property |
Function name | Variable name if anon | Variable name |
Pick arrow for callbacks and short helpers; pick declaration for top-level / named functions you might recurse into; rarely use a named expression unless you specifically want a different external/internal name.
2. Parameters
2.1 Defaults
function greet(name = 'world', loud = false) {
return loud ? `HI ${name.toUpperCase()}` : `Hi ${name}`;
}
greet(); // 'Hi world'
greet(undefined); // 'Hi world' — undefined triggers default; null doesn't
greet('Yu', true); // 'HI YU'
Defaults are evaluated per call in scope of the parameter list:
function f(x, y = x * 2) { return y; }
f(3); // 6
2.2 Rest Parameters
Collect remaining args into an array:
function sum(...nums) { return nums.reduce((a, b) => a + b, 0); }
sum(1, 2, 3, 4); // 10
function tag(strings, ...values) { /* template tag */ }
Rest must be the last parameter.
2.3 Spread (the call-site complement)
const args = [1, 2, 3];
sum(...args); // sum(1, 2, 3)
[...arr1, ...arr2]; // concatenate
{ ...obj1, ...obj2 }; // merge (later wins on conflict)
Spread also works in array literals, object literals, and new expressions.
2.4 Destructuring Parameters
function draw({ x = 0, y = 0, color = 'black' } = {}) {
// ...
}
draw({ x: 10, color: 'red' });
draw(); // works because of the `= {}` default
= {} is essential — without it, calling draw() would destructure undefined and throw.
Array destructuring works too:
function swap([a, b]) { return [b, a]; }
swap([1, 2]); // [2, 1]
2.5 The Legacy arguments Object
Inside non-arrow functions only:
function f() {
console.log(arguments); // [Arguments] { 0: …, 1: … }
console.log(arguments.length);
console.log([...arguments]); // convert to real array
}
Array-like but not an array — needs spread or Array.from to use array methods. Mostly obsolete; rest parameters are clearer.
3. Calling Functions
greet('Yu'); // direct
const fn = greet; fn('Yu'); // through a variable
arr.map(greet); // as a callback
greet.call(thisArg, 'Yu'); // explicit this + args list
greet.apply(thisArg, ['Yu']); // explicit this + args array
const bound = greet.bind(thisArg, 'Yu'); // returns new function with bound this/args
bound(); // calls greet.call(thisArg, 'Yu')
new Greeter('Yu'); // constructor call
.bind() is one-shot — re-binding a bound function doesn't change the original binding.
3.1 IIFE (Immediately Invoked Function Expression)
(function () {
// private scope
})();
(() => {
// arrow version
})();
Once essential for module-style encapsulation. ES modules replaced 95% of the use cases.
4. this Binding
this is determined by how a function is called, not where it's defined — except for arrow functions, which freeze this at definition.
| Call form | this |
|---|---|
fn() (plain) |
undefined in strict mode; globalThis otherwise |
obj.fn() |
obj |
fn.call(x, …) / fn.apply(x, …) |
x |
new Fn(…) |
newly created object |
| Arrow function | inherited from enclosing scope (locked at definition) |
| Event handler (DOM) | the element |
fn.bind(x)() |
x |
const obj = {
name: 'A',
greet() { return `Hi ${this.name}`; },
greetArrow: () => `Hi ${this.name}`,
};
obj.greet(); // 'Hi A'
const g = obj.greet;
g(); // 'Hi undefined' — lost binding
obj.greetArrow(); // arrow's this was captured at definition, not from the call:
// in an ES module → TypeError (top-level this is undefined)
// in a browser classic script → 'Hi ' (window.name defaults to '')
Common bug — passing a method as callback loses this:
button.addEventListener('click', obj.greet); // 'this' will be the button
button.addEventListener('click', () => obj.greet()); // works — arrow captures obj
button.addEventListener('click', obj.greet.bind(obj)); // works — explicit bind
Class methods are not auto-bound — same pitfall. Class field arrow methods are auto-bound:
class Counter {
count = 0;
incrementMethod() { this.count++; } // not bound to instance
incrementArrow = () => { this.count++; }; // bound — arrow captures `this`
}
The auto-bound pattern is heavier (per-instance function), but solves the React onClick={this.handler} issue.
5. Scope
JavaScript has three scope levels:
| Scope | Bound by | Created by |
|---|---|---|
| Global | the file / module / page | top-level code |
| Function | function body | function keyword |
| Block | { … } |
any block (also if, for, try, …) |
function outer() { // function scope
let a = 1;
if (true) { // block scope
let b = 2;
const c = 3;
var d = 4; // var leaks to function scope
}
console.log(a); // 1
// console.log(b); // ReferenceError
console.log(d); // 4 — var ignored block
}
let and const are block-scoped. var is function-scoped (or global). var leaks out of blocks — main reason to avoid it.
Lexical scope — inner scopes can see outer scopes' bindings; outer can't see inner. Resolution walks outward at runtime through the scope chain.
const a = 1;
function outer() {
const b = 2;
function inner() {
const c = 3;
return a + b + c; // 1 + 2 + 3 = 6
}
return inner();
}
6. Hoisting
JavaScript "moves" certain declarations to the top of their scope before execution. Mental model: compilation registers all bindings before any statement runs.
| Construct | Hoisting |
|---|---|
var x = … |
Declaration hoisted, initialised to undefined. Assignment stays. |
let x = … / const x = … |
Binding hoisted; access throws ReferenceError (TDZ) until declaration line. |
function f() {} |
Whole function hoisted. Callable before its source line. |
class C {} |
Hoisted but TDZ — can't reference before declaration. |
| Function expression / arrow | Only the variable hoists; function value comes at assignment time. |
console.log(a); // undefined — var hoisted, no value yet
var a = 1;
console.log(b); // ReferenceError (TDZ)
let b = 1;
hello(); // works — function declaration hoists with body
function hello() { console.log('hi'); }
bye(); // TypeError — bye is undefined, not a function (yet)
var bye = function () { console.log('bye'); };
Modern code rarely depends on hoisting beyond function declarations. let/const make the TDZ a feature — using a variable before it's declared is almost always a bug.
7. Closures
A closure is a function plus the lexical environment it was defined in. The function keeps access to those variables even after the outer function returns.
function makeCounter() {
let count = 0;
return {
increment() { count++; },
get() { return count; },
};
}
const c = makeCounter();
c.increment();
c.increment();
c.get(); // 2
count lives as long as the returned methods do. This is the basic "private state" pattern in JS.
7.1 The Classic Loop Trap
// Old, broken with var
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// Logs: 3, 3, 3 — all share the same var i
// Fixed with let — each iteration creates a fresh binding
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// Logs: 0, 1, 2
let in the for head allocates a new binding per iteration, so each callback captures a different i. One of the headline reasons to drop var.
7.2 Common Closure Uses
- Private state — counters, caches, configuration.
- Currying / partial application —
const add5 = x => y => x + y;. - Memoization — wrap a function with a cache it owns:
function memoize(fn) {
const cache = new Map();
return (key) => {
if (cache.has(key)) return cache.get(key);
const result = fn(key);
cache.set(key, result);
return result;
};
}
- Module pattern (pre-ESM) — IIFE that exposes a small API while keeping internals hidden.
- Event handlers —
onClickcallbacks that need to remember which row they belong to.
7.3 Memory Implications
A closure holds onto its lexical environment. If a closure references a large object you don't actually use, the GC can't free it. Audit closures in long-lived code (event listeners, intervals, caches) to make sure they don't pin unnecessary state.
8. Higher-Order Functions
Functions that take other functions, return other functions, or both. Most array methods are higher-order.
// Take a function
arr.map(x => x * 2);
// Return a function
const greaterThan = (n) => (x) => x > n;
arr.filter(greaterThan(10));
// Composition
const compose = (...fns) => (x) => fns.reduceRight((v, fn) => fn(v), x);
const trim = s => s.trim();
const upper = s => s.toUpperCase();
const shout = compose(upper, trim);
shout(' hi '); // 'HI'
Functional helpers are first-class in JS — most "library missing" complaints in 2026 are unwarranted; the language has the primitives.
9. References
- MDN Functions — https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions
- MDN Closures — https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures
- MDN
this— https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this - See also: javascript-objects-prototypes-and-classes, javascript-async-and-event-loop