Functions and closures
JavaScript functions are first-class values — admit assignment, passing as arguments, returning, and substantial closure over enclosing scope. Three principal forms: function declarations (function name() {} — hoisted), function expressions (const f = function() {} — assigned to a binding), and arrow functions (const f = () => {} — concise, lexical this). The principal features: default parameters, rest/spread (...), destructuring parameters, the substantial closure mechanism over enclosing scope, higher-order functions (functions that take or return functions). The combination — first-class functions, arrow functions with lexical this, default and rest parameters, the substantial closure surface, the higher-order patterns admitting substantial functional code — is the substance of JavaScript’s function surface.
Function declarations
The principal forms:
// Function declaration (hoisted):
function add(a, b) {
return a + b;
}
// Function expression:
const add = function (a, b) {
return a + b;
};
// Named function expression:
const add = function addImpl(a, b) {
return a + b;
};
// Arrow function:
const add = (a, b) => a + b;
// Multi-statement arrow:
const greet = (name) => {
const greeting = `Hello, ${name}`;
return greeting;
};
// Single param (parens optional):
const square = n => n * n;
// No params:
const noArgs = () => 42;
// Returning object literal (need parens):
const makeUser = (name) => ({ name, role: "user" });
Arrow vs function
The principal differences:
| Function | Arrow | |
|---|---|---|
this binding | Own (depends on call site) | Lexical (inherited) |
arguments | Yes | No |
new (constructor) | Yes | No |
| Hoisting | Declaration: yes; expression: no | No |
| Generator | Yes (function*) | No |
| Method shorthand in class | n/a | Use as field |
const obj = {
count: 0,
// Method (this is obj):
method() {
console.log(this.count);
},
// Arrow (this is whatever it was at definition):
arrow: () => {
console.log(this); // not obj!
}
};
obj.method(); // 0
obj.arrow(); // depends on outer this
Default parameters
function greet(name = "world", greeting = "Hello") {
return `${greeting}, ${name}`;
}
greet(); // "Hello, world"
greet("Alice"); // "Hello, Alice"
greet("Alice", "Hi"); // "Hi, Alice"
// Default uses earlier params:
function makeUrl(host, port = 80, path = "/") {
return `http://${host}:${port}${path}`;
}
// Default is computed:
function fetch(url, options = {}) {
const { method = "GET", headers = {} } = options;
// ...
}
// undefined uses default; null does not:
greet(undefined, "Hi"); // "Hi, world"
greet(null, "Hi"); // "Hi, null"
Rest parameters
The ... admits rest parameters — collect remaining args:
function sum(...nums) {
return nums.reduce((a, b) => a + b, 0);
}
sum(); // 0
sum(1, 2, 3); // 6
sum(1, 2, 3, 4, 5); // 15
// Combined with named:
function logAll(level, ...messages) {
messages.forEach(m => console.log(`[${level}]`, m));
}
logAll("INFO", "msg1", "msg2");
// Spread to call:
const args = [1, 2, 3];
sum(...args); // 6
The rest parameter must be the last parameter.
Destructuring parameters
function greet({ name, age }) {
console.log(`${name} is ${age}`);
}
greet({ name: "Alice", age: 30 });
// With defaults:
function fetch(url, { method = "GET", headers = {}, body = null } = {}) {
// ...
}
fetch("/api"); // uses all defaults
fetch("/api", { method: "POST" });
// Array destructuring:
function distance([x1, y1], [x2, y2]) {
return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
}
distance([0, 0], [3, 4]); // 5
this binding
The principal this rules for function (not arrow):
// Direct call: this is undefined (strict mode) or globalThis (sloppy):
function foo() {
console.log(this);
}
foo();
// Method call: this is the object:
const obj = { foo };
obj.foo(); // this is obj
// Constructor call: this is the new instance:
new foo();
// .call/.apply/.bind:
foo.call(thisArg, arg1, arg2);
foo.apply(thisArg, [arg1, arg2]);
const bound = foo.bind(thisArg, arg1);
bound(arg2); // foo.call(thisArg, arg1, arg2)
The arrow function inherits this from the enclosing scope:
class Counter {
constructor() {
this.count = 0;
}
// With arrow, this is always the instance:
increment = () => {
this.count += 1;
};
// With method, this depends on call site:
decrement() {
this.count -= 1;
}
}
const c = new Counter();
const inc = c.increment;
inc(); // works (this still c)
const dec = c.decrement;
dec(); // TypeError (this is undefined)
// Defence: bind:
const decBound = c.decrement.bind(c);
decBound(); // works
Closures
Functions capture variables from their lexical scope:
function makeCounter() {
let count = 0;
return function () {
count += 1;
return count;
};
}
const counter = makeCounter();
counter(); // 1
counter(); // 2
counter(); // 3
// Each call to makeCounter produces a new closure:
const c1 = makeCounter();
const c2 = makeCounter();
c1(); // 1
c1(); // 2
c2(); // 1 (independent)
Closures admit substantial state encapsulation:
function makeAccount(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 = makeAccount(100);
account.deposit(50); // 150
// balance is not directly accessible
Higher-order functions
Functions that take or return functions:
// Take function:
function applyTwice(fn, x) {
return fn(fn(x));
}
applyTwice(n => n + 1, 5); // 7
// Return function:
function adder(n) {
return (x) => x + n;
}
const add5 = adder(5);
add5(3); // 8
// Both:
function compose(f, g) {
return (x) => f(g(x));
}
const inc = n => n + 1;
const double = n => n * 2;
const incDouble = compose(double, inc);
incDouble(5); // (5 + 1) * 2 = 12
arguments (legacy)
Inside a function (not arrow), arguments admits all passed arguments:
function legacy() {
console.log(arguments.length); // count
console.log(arguments[0]); // first
return Array.from(arguments).reduce((a, b) => a + b, 0);
}
legacy(1, 2, 3); // 6
The conventional contemporary discipline uses rest parameters (...args) — admit substantial cleaner code:
function modern(...args) {
return args.reduce((a, b) => a + b, 0);
}
IIFE
The pre-modules pattern:
(function () {
const private = "hidden";
// ...
})();
// Or:
(() => {
// ...
})();
// Or:
+function () {
// ...
}();
The mechanism admits module-like scoping; conventional in legacy code. Modern code uses ES modules.
Generator functions
The function* admits generators — functions that produce values via yield:
function* range(start, stop, step = 1) {
for (let i = start; i < stop; i += step) {
yield i;
}
}
for (const n of range(1, 10)) {
console.log(n); // 1, 2, ..., 9
}
const arr = [...range(0, 5)]; // [0, 1, 2, 3, 4]
// Manual iteration:
const iter = range(0, 5);
iter.next(); // { value: 0, done: false }
iter.next(); // { value: 1, done: false }
Generators admit substantial lazy iteration and substantial coroutine-style patterns.
Async functions
async function fetchUser(id) {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
}
const user = await fetchUser(123);
Treated in Async and promises.
Common patterns
Default arg with object
function fetch(url, options = {}) {
const {
method = "GET",
headers = {},
body = null,
timeout = 30000
} = options;
// ...
}
fetch("/api");
fetch("/api", { method: "POST", body: data });
Higher-order: map / filter / reduce
const arr = [1, 2, 3, 4, 5];
const doubled = arr.map(x => x * 2);
const evens = arr.filter(x => x % 2 === 0);
const sum = arr.reduce((a, b) => a + b, 0);
// Chained:
const result = arr
.filter(x => x > 1)
.map(x => x * 2)
.reduce((a, b) => a + b, 0);
Memoization
function memoize(fn) {
const cache = new Map();
return function (key) {
if (cache.has(key)) return cache.get(key);
const value = fn(key);
cache.set(key, value);
return value;
};
}
const fib = memoize(n => {
if (n < 2) return n;
return fib(n - 1) + fib(n - 2);
});
Debounce
function debounce(fn, delay) {
let timer;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
const handleSearch = debounce((query) => {
fetch(`/api/search?q=${query}`).then(...);
}, 300);
input.addEventListener("input", (e) => handleSearch(e.target.value));
Throttle
function throttle(fn, interval) {
let lastCall = 0;
return function (...args) {
const now = Date.now();
if (now - lastCall >= interval) {
lastCall = now;
fn.apply(this, args);
}
};
}
const handleScroll = throttle(() => {
console.log(window.scrollY);
}, 100);
window.addEventListener("scroll", handleScroll);
Curry
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
}
return (...moreArgs) => curried(...args, ...moreArgs);
};
}
const add = (a, b, c) => a + b + c;
const curried = curry(add);
curried(1)(2)(3); // 6
curried(1, 2)(3); // 6
curried(1)(2, 3); // 6
Compose / pipe
const compose = (...fns) => (x) => fns.reduceRight((acc, fn) => fn(acc), x);
const pipe = (...fns) => (x) => fns.reduce((acc, fn) => fn(acc), x);
const transform = pipe(
s => s.trim(),
s => s.toLowerCase(),
s => s.replace(/\s+/g, "-")
);
transform(" Hello World "); // "hello-world"
Builder pattern
class QueryBuilder {
constructor() {
this.parts = [];
}
where(condition) {
this.parts.push(`WHERE ${condition}`);
return this;
}
orderBy(field) {
this.parts.push(`ORDER BY ${field}`);
return this;
}
limit(n) {
this.parts.push(`LIMIT ${n}`);
return this;
}
build() {
return this.parts.join(" ");
}
}
const sql = new QueryBuilder()
.where("active = true")
.orderBy("created_at DESC")
.limit(10)
.build();
Event handlers with arrow
class TodoApp {
constructor() {
this.todos = [];
document.getElementById("add").addEventListener("click", this.addTodo);
document.getElementById("clear").addEventListener("click", () => this.clear());
}
addTodo = () => {
// arrow as field — `this` is always the instance
this.todos.push(/* ... */);
};
clear() {
this.todos = [];
}
}
Async with arrow
const fetchAll = async (urls) => {
const results = await Promise.all(urls.map(url => fetch(url).then(r => r.json())));
return results;
};
Once
function once(fn) {
let called = false;
let result;
return function (...args) {
if (!called) {
called = true;
result = fn.apply(this, args);
}
return result;
};
}
const init = once(() => {
console.log("initialised");
return setupApp();
});
init(); // "initialised"
init(); // returns same result; no log
Partial application
function partial(fn, ...preset) {
return (...later) => fn(...preset, ...later);
}
const greet = (greeting, name) => `${greeting}, ${name}`;
const sayHi = partial(greet, "Hi");
sayHi("Alice"); // "Hi, Alice"
bind for substantial event handler context
class Component {
constructor() {
this.count = 0;
this.handleClick = this.handleClick.bind(this); // bind once in constructor
}
handleClick() {
this.count++;
}
}
// Or arrow as field (modern preferred):
class Component {
count = 0;
handleClick = () => {
this.count++;
};
}
Variadic with spread
function logAll(...args) {
console.log(...args); // forward
}
function call(fn, ...args) {
return fn(...args); // spread to call
}
Optional callback
function process(items, onProgress) {
items.forEach((item, i) => {
doWork(item);
onProgress?.(i + 1, items.length); // optional chain on call
});
}
process(items); // no callback
process(items, (current, total) => {
console.log(`${current}/${total}`);
});
Function with named arguments via object
function fetch({
url,
method = "GET",
headers = {},
body = null,
timeout = 30000
} = {}) {
// ...
}
fetch({
url: "/api/users",
method: "POST",
body: JSON.stringify(data)
});
The pattern admits substantial readability for substantial parameter lists.
A note on the conventional discipline
The contemporary JavaScript functions advice:
- Use arrow functions for short callbacks and methods needing lexical
this. - Use function declarations for top-level named functions.
- Use default parameters over
||for substantial defaults. - Use rest parameters (
...args) overarguments. - Use destructuring parameters for substantial readable signatures.
- Use
constwith arrow for declared functions. - Use closures for state encapsulation.
- Use higher-order functions (map/filter/reduce, debounce, throttle).
- Use
bindor arrow to fixthisfor handlers. - Avoid
function-keyword methods in classes when arrow-as-field admits substantialthissimplification. - Use
Promise.allandawaitover manual callback patterns.
The combination — first-class functions with three forms (declaration, expression, arrow), default and rest parameters, destructuring parameters, the substantial closure mechanism, the lexical this of arrows, the generator and async forms — is the substance of JavaScript’s function surface. The discipline produces concise, expressive, well-encapsulated code with substantial flexibility for substantial higher-order patterns and substantial async work.