Polyglot
Languages Web (HTML / CSS / JS) functions
Web (HTML / CSS / JS) § functions

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:

FunctionArrow
this bindingOwn (depends on call site)Lexical (inherited)
argumentsYesNo
new (constructor)YesNo
HoistingDeclaration: yes; expression: noNo
GeneratorYes (function*)No
Method shorthand in classn/aUse 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) over arguments.
  • Use destructuring parameters for substantial readable signatures.
  • Use const with arrow for declared functions.
  • Use closures for state encapsulation.
  • Use higher-order functions (map/filter/reduce, debounce, throttle).
  • Use bind or arrow to fix this for handlers.
  • Avoid function-keyword methods in classes when arrow-as-field admits substantial this simplification.
  • Use Promise.all and await over 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.