JavaScript Functions

Functions are the building blocks of every JavaScript program. Learn declarations, expressions, arrow functions, closures, and higher-order functions - the patterns that power modern JavaScript.

Beginner 12 min read 13 examples

Function Declaration

A function declaration defines a named function using the function keyword. Declarations are hoisted - you can call them before they appear in the code.

JavaScript
// Can be called before its definition (hoisted)
console.log(greet("Alice")); // "Hello, Alice!"

function greet(name) {
    return `Hello, ${name}!`;
}

// Function with multiple statements
function calculateTax(price, rate) {
    const tax = price * rate;
    const total = price + tax;
    return { tax, total };
}

const result = calculateTax(100, 0.2);
console.log(result.tax);   // 20
console.log(result.total); // 120

Function Expression

A function expression assigns a function to a variable. It is NOT hoisted - it must be defined before it is called.

JavaScript
// sayHi(); // TypeError: Cannot access before initialization

const sayHi = function(name) {
    return `Hi, ${name}!`;
};

console.log(sayHi("Bob")); // "Hi, Bob!"

// Named function expression (name visible inside function only)
const factorial = function fact(n) {
    return n <= 1 ? 1 : n * fact(n - 1); // can reference 'fact' internally
};
console.log(factorial(5)); // 120
// console.log(fact(5));   // ReferenceError - 'fact' not in outer scope

// Functions are first-class values - assign, pass, return
const double = function(n) { return n * 2; };
const numbers = [1, 2, 3, 4];
console.log(numbers.map(double)); // [2, 4, 6, 8]

Arrow Functions

Arrow functions (=>) are a shorter syntax for function expressions. They also differ in how they handle this - they inherit this from the enclosing scope instead of defining their own.

JavaScript
// Full arrow function
const add = (a, b) => {
    return a + b;
};

// Concise body: single expression, implicit return (no braces, no return)
const add2 = (a, b) => a + b;

// Single parameter: parentheses optional
const double = n => n * 2;

// No parameters: empty parens required
const getTime = () => new Date().toISOString();

// Returning an object literal: wrap in parentheses
const makeUser = (name, age) => ({ name, age });
console.log(makeUser("Alice", 28)); // { name: "Alice", age: 28 }

// Arrow functions in array methods (most common use)
const nums = [1, 2, 3, 4, 5];
const evens  = nums.filter(n => n % 2 === 0);   // [2, 4]
const doubled = nums.map(n => n * 2);             // [2,4,6,8,10]
const sum     = nums.reduce((acc, n) => acc + n, 0); // 15
Arrow Functions and this

Arrow functions do not have their own this. They inherit this from the surrounding lexical scope. This makes them ideal for callbacks inside class methods (where you want this to refer to the class instance). Do not use arrow functions as object methods when you need this to refer to the object itself.

Parameters and Arguments

JavaScript
// Extra arguments are ignored; missing ones become undefined
function log(a, b, c) {
    console.log(a, b, c);
}
log(1, 2, 3, 4); // 1 2 3  (4 is ignored)
log(1, 2);       // 1 2 undefined

// arguments object (regular functions only, not arrow)
function sum() {
    let total = 0;
    for (const arg of arguments) {
        total += arg;
    }
    return total;
}
console.log(sum(1, 2, 3, 4)); // 10

// Passing objects and arrays (by reference)
function addProp(obj) {
    obj.added = true; // modifies the original!
}
const myObj = { x: 1 };
addProp(myObj);
console.log(myObj.added); // true

Default and Rest Parameters

JavaScript
// Default parameters (ES6)
function greet(name = "Guest", greeting = "Hello") {
    return `${greeting}, ${name}!`;
}
console.log(greet());              // "Hello, Guest!"
console.log(greet("Alice"));       // "Hello, Alice!"
console.log(greet("Bob", "Hi"));   // "Hi, Bob!"
console.log(greet(undefined, "Hey")); // "Hey, Guest!" (undefined triggers default)
// console.log(greet(null, "Hey"));    // "Hey, null!" (null does NOT trigger default)

// Default can reference earlier parameters
function repeat(str, times = str.length) {
    return str.repeat(times);
}

// Rest parameters (...) - collects remaining args into an array
function sumAll(first, ...rest) {
    return rest.reduce((acc, n) => acc + n, first);
}
console.log(sumAll(1, 2, 3, 4, 5)); // 15
console.log(sumAll(10));             // 10

// Rest must be the last parameter
function tag(label, ...values) {
    return `[${label}] ${values.join(", ")}`;
}
console.log(tag("info", "a", "b", "c")); // "[info] a, b, c"

Return Values

JavaScript
// Functions without return statement return undefined
function doNothing() {}
console.log(doNothing()); // undefined

// return exits the function immediately
function classify(n) {
    if (n < 0) return "negative";
    if (n === 0) return "zero";
    return "positive";   // only reached if n > 0
}
console.log(classify(-5)); // "negative"

// Return multiple values via an object or array
function minMax(arr) {
    return {
        min: Math.min(...arr),
        max: Math.max(...arr)
    };
}
const { min, max } = minMax([3, 1, 4, 1, 5, 9, 2]);
console.log(min, max); // 1 9

// Return a function (factory pattern)
function multiplier(factor) {
    return n => n * factor;
}
const triple = multiplier(3);
console.log(triple(5)); // 15
console.log(triple(7)); // 21

Higher-Order Functions

A higher-order function either accepts a function as an argument or returns a function. This enables powerful abstractions and reusable logic.

JavaScript
// Takes a function as argument
function applyTwice(fn, value) {
    return fn(fn(value));
}
const double = n => n * 2;
console.log(applyTwice(double, 3)); // 12 (3 -> 6 -> 12)

// Returns a function
function makeGreeter(greeting) {
    return name => `${greeting}, ${name}!`;
}
const sayHello = makeGreeter("Hello");
const sayHi    = makeGreeter("Hi");
console.log(sayHello("Alice")); // "Hello, Alice!"
console.log(sayHi("Bob"));     // "Hi, Bob!"

// Function composition
const pipe = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x);

const process = pipe(
    x => x * 2,
    x => x + 10,
    x => x.toString()
);
console.log(process(5)); // "20" (5 -> 10 -> 20 -> "20")

// Memoization - cache results
function memoize(fn) {
    const cache = new Map();
    return function(...args) {
        const key = JSON.stringify(args);
        if (cache.has(key)) return cache.get(key);
        const result = fn.apply(this, args);
        cache.set(key, result);
        return result;
    };
}
const expensiveCalc = memoize(n => n ** 2);
console.log(expensiveCalc(10)); // 100 (computed)
console.log(expensiveCalc(10)); // 100 (from cache)

Closures

A closure is a function that retains access to variables from its outer scope even after the outer function has returned. Every function in JavaScript forms a closure.

JavaScript
// Classic closure: counter with private state
function makeCounter(start = 0) {
    let count = start;  // private variable
    return {
        increment() { return ++count; },
        decrement() { return --count; },
        reset()     { count = start; },
        value()     { return count; }
    };
}
const counter = makeCounter(10);
console.log(counter.increment()); // 11
console.log(counter.increment()); // 12
console.log(counter.decrement()); // 11
// count is not accessible from outside - true encapsulation

// Closure over loop variables - common gotcha
const fns = [];
for (let i = 0; i < 3; i++) {
    fns.push(() => i);  // 'let' creates new binding per iteration
}
console.log(fns[0]()); // 0
console.log(fns[1]()); // 1
console.log(fns[2]()); // 2
// With 'var': all would print 3 (shared variable)

// Practical closure: event handler with captured context
function attachHandler(button, message) {
    button.addEventListener("click", () => {
        console.log(message); // captures 'message' from outer scope
    });
}

Function Types Compared

FeatureDeclarationExpressionArrow
Syntaxfunction f(){}const f = function(){}const f = () => {}
HoistedYes (fully)NoNo
Own thisYesYesNo (lexical)
argumentsYesYesNo
Used as constructorYesYesNo
Implicit returnNoNoYes (concise body)
Best forTop-level reusable functionsConditional or assigned functionsCallbacks and short functions

Frequently Asked Questions

Function declarations (function foo() {}) are hoisted - you can call them before they appear in the code. Function expressions (const foo = function() {}) are not hoisted and must be defined before being called. In modern code, the choice often comes down to style: declarations for top-level reusable functions, expressions when assigning functions to variables or passing them as arguments.

Use arrow functions for callbacks, array methods, and short functions where you want concise syntax. Use regular functions when you need: (1) a constructor with new, (2) the function's own this (arrow functions inherit this from their enclosing scope), (3) arguments object, or (4) named function references in stack traces. In practice, most modern code uses arrow functions for callbacks and regular declarations for standalone functions.

A closure is a function that "remembers" the variables from its outer scope even after the outer function has finished running. Every function in JavaScript forms a closure. They are useful for data privacy (encapsulating state), factory functions, and callback patterns. The classic example: a counter function that returns an increment function - the inner function retains access to the count variable.

A higher-order function is a function that either takes another function as an argument or returns a function (or both). Array methods like map, filter, and reduce are higher-order functions - they take callback functions as arguments. Functions that return other functions (like factories or decorators) are also higher-order. This is a core concept in functional programming.