Function Declarations and Expressions

โ–ถ Try It Yourself

Functions are the primary building block of JavaScript programs. They let you name and reuse a block of logic, accept inputs via parameters, and return outputs. JavaScript is unique in treating functions as first-class values โ€” they can be assigned to variables, passed to other functions, and returned from functions. In this lesson you will master function declarations, function expressions, default parameters, rest parameters, and the critical differences between them.

Function Types Comparison

Type Syntax Hoisted? Has its own this?
Declaration function name() { } Yes โ€” callable before declaration Yes
Expression const name = function() { } No โ€” must declare before calling Yes
Named expression const name = function inner() { } No Yes โ€” inner name available inside only
Arrow function const name = () => { } No No โ€” inherits this from outer scope
Method shorthand { name() { } } No Yes

Parameters and Arguments

Feature Syntax Example
Default parameter function f(x = 10) Used when argument is undefined
Rest parameter function f(...args) Collects remaining args into array
Destructured parameter function f({ name, age }) Unpacks object argument directly
arguments object arguments[0] Legacy โ€” only in regular functions, not arrows

Return Values

Scenario Return Value Notes
Explicit return The expression after return Function exits immediately at return
No return statement undefined JavaScript implicitly returns undefined
Empty return undefined return; โ€” used to exit early
Return object literal Must wrap in parens in arrow functions () => ({ key: value })
Note: Function declarations are hoisted entirely โ€” the function name AND its body are moved to the top of their scope. This means you can call a declared function before the line it is defined on. Function expressions assigned to const or let are NOT hoisted (they are in the TDZ until the assignment line), so calling them before their declaration throws a ReferenceError.
Tip: Use default parameters instead of the old param = param || default pattern. The old pattern breaks when a legitimate falsy value like 0 or false is passed โ€” it gets replaced by the default. Default parameters only activate when the argument is undefined, not for any falsy value: function greet(name = 'Guest') uses ‘Guest’ only when name is not passed at all.
Warning: Avoid using the arguments object in modern JavaScript. It is not available in arrow functions, it is array-like but not a real array (no .map(), .filter()), and it is confusing to read. Use the rest parameter ...args instead โ€” it is a real array and works in all function types.

Basic Example

// โ”€โ”€ Function declaration โ€” hoisted โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
console.log(add(3, 4));   // 7 โ€” works before declaration due to hoisting

function add(a, b) {
    return a + b;
}

// โ”€โ”€ Function expression โ€” not hoisted โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
const multiply = function(a, b) {
    return a * b;
};
console.log(multiply(3, 4));  // 12

// โ”€โ”€ Default parameters โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
function greet(name = 'Guest', greeting = 'Hello') {
    return `${greeting}, ${name}!`;
}

console.log(greet());                    // 'Hello, Guest!'
console.log(greet('Alice'));             // 'Hello, Alice!'
console.log(greet('Bob', 'Welcome'));    // 'Welcome, Bob!'
console.log(greet(undefined, 'Hi'));     // 'Hi, Guest!' โ€” undefined triggers default

// โ”€โ”€ Rest parameters โ€” collect remaining args โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
function sum(...numbers) {
    return numbers.reduce((total, n) => total + n, 0);
}

console.log(sum(1, 2, 3));           // 6
console.log(sum(10, 20, 30, 40));    // 100
console.log(sum());                  // 0

// โ”€โ”€ Destructured object parameter โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
function createUserCard({ name, age, role = 'viewer', isActive = true } = {}) {
    return {
        display:  `${name} (${age}) โ€” ${role}`,
        isActive,
        initials: name.split(' ').map(w => w[0]).join('').toUpperCase(),
    };
}

console.log(createUserCard({ name: 'Alice Smith', age: 30, role: 'admin' }));
// { display: 'Alice Smith (30) โ€” admin', isActive: true, initials: 'AS' }

// โ”€โ”€ Functions as values โ€” assigned, passed, returned โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
const operations = {
    add:      (a, b) => a + b,
    subtract: (a, b) => a - b,
    multiply: (a, b) => a * b,
    divide:   (a, b) => b !== 0 ? a / b : null,
};

function calculate(op, a, b) {
    const fn = operations[op];
    if (!fn) throw new Error(`Unknown operation: ${op}`);
    return fn(a, b);
}

console.log(calculate('add',      10, 3));   // 13
console.log(calculate('multiply', 10, 3));   // 30
console.log(calculate('divide',   10, 0));   // null

// โ”€โ”€ Early return pattern โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
function getDiscount(user, cartTotal) {
    if (!user)               return 0;
    if (cartTotal <= 0)      return 0;
    if (user.role === 'vip') return 0.20;
    if (user.loyaltyYears >= 3) return 0.10;
    if (cartTotal >= 100)    return 0.05;
    return 0;
}

console.log(getDiscount({ role: 'vip', loyaltyYears: 5 }, 50));  // 0.20
console.log(getDiscount({ role: 'user', loyaltyYears: 4 }, 50)); // 0.10
console.log(getDiscount({ role: 'user', loyaltyYears: 1 }, 150));// 0.05

How It Works

Step 1 โ€” Hoisting Lifts Declarations to the Top

During the compilation phase, JavaScript scans for function declarations and registers them before any code runs. That is why add(3, 4) works before the function is written. Function expressions (assigned to const) are not hoisted โ€” the variable is in the TDZ until the assignment line executes.

Step 2 โ€” Default Parameters Only Trigger on undefined

When you call greet(undefined, 'Hi'), the first argument is explicitly undefined โ€” the default 'Guest' kicks in. But greet(null, 'Hi') would give 'Hi, null!' โ€” because null is a value, not undefined. Defaults activate only for missing or explicitly-undefined arguments.

Step 3 โ€” Rest Parameters Collect Extras into a Real Array

...numbers collects all passed arguments into a genuine array. Unlike the legacy arguments object, it supports every array method. The rest parameter must be the last parameter โ€” function f(first, ...rest) puts the first argument in first and everything else in rest.

Step 4 โ€” Destructured Parameters Unpack Objects Inline

{ name, age, role = 'viewer' } as a parameter destructures the passed object directly in the signature, with inline defaults for missing keys. The = {} at the end provides a default for the whole argument โ€” so calling the function with no arguments does not throw a TypeError trying to destructure undefined.

Step 5 โ€” Functions Are First-Class Values

The operations object stores functions as values. calculate looks up the right function by name and calls it. This is the foundation of the strategy pattern โ€” swapping behaviour by passing different functions. It is also how event listeners, array methods, and callbacks all work.

Real-World Example: Form Field Validator Factory

// validator-factory.js

// Factory function โ€” returns a configured validator function
function createValidator(rules) {
    return function validate(data) {
        const errors = {};

        for (const [field, fieldRules] of Object.entries(rules)) {
            const value = data[field];

            for (const rule of fieldRules) {
                const error = rule(value, field, data);
                if (error) {
                    errors[field] = error;
                    break;  // first failing rule wins per field
                }
            }
        }

        return {
            valid:  Object.keys(errors).length === 0,
            errors,
        };
    };
}

// Reusable rule functions
const required   = (v, f)    => (v == null || String(v).trim() === '') ? `${f} is required` : null;
const minLen     = (min) =>
    (v, f) => (String(v ?? '').length < min) ? `${f} must be at least ${min} characters` : null;
const maxLen     = (max) =>
    (v, f) => (String(v ?? '').length > max) ? `${f} must be ${max} characters or fewer` : null;
const isEmail    = (v, f)    => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v ?? '') ? null : `${f} must be a valid email`;
const isNumeric  = (v, f)    => Number.isFinite(Number(v)) ? null : `${f} must be a number`;
const minVal     = (min) =>
    (v, f) => Number(v) >= min ? null : `${f} must be at least ${min}`;

// Build a reusable validator for a registration form
const validateRegistration = createValidator({
    username: [required, minLen(3), maxLen(20)],
    email:    [required, isEmail],
    age:      [required, isNumeric, minVal(13)],
    password: [required, minLen(8)],
});

console.log(validateRegistration({ username: 'al', email: 'bad', age: '10', password: 'short' }));
// { valid: false, errors: { username: '...3 chars', email: '...valid email', age: '...13', password: '...8 chars' } }

console.log(validateRegistration({ username: 'alice', email: 'a@b.com', age: '25', password: 'secure123' }));
// { valid: true, errors: {} }

Common Mistakes

Mistake 1 โ€” Calling a function expression before its declaration

โŒ Wrong โ€” function expressions are not hoisted:

console.log(double(5));           // ReferenceError โ€” cannot access before init
const double = (n) => n * 2;

โœ… Correct โ€” declare before calling, or use a function declaration:

const double = (n) => n * 2;
console.log(double(5));           // 10 โ€” declared first

Mistake 2 โ€” Using || for default values in function body

โŒ Wrong โ€” || replaces 0 and false with the default:

function setVolume(level) {
    level = level || 50;   // if level is 0, it becomes 50!
}

โœ… Correct โ€” use a default parameter:

function setVolume(level = 50) {
    // level is 0 when 0 is passed โ€” default only for undefined
}

Mistake 3 โ€” Forgetting to return from a function

โŒ Wrong โ€” missing return means the function produces undefined:

function double(n) {
    n * 2;   // calculated but not returned!
}
console.log(double(5));   // undefined

โœ… Correct โ€” explicitly return the result:

function double(n) {
    return n * 2;
}
console.log(double(5));   // 10

▶ Try It Yourself

Quick Reference

Concept Syntax Notes
Declaration function name(params) { return x; } Hoisted โ€” callable anywhere in scope
Expression const name = function(params) { } Not hoisted โ€” declare before use
Default param function f(x = 10) Activates only when arg is undefined
Rest param function f(...args) Real array โ€” must be last param
Destructured param function f({ name, age = 0 } = {}) Unpack object inline with defaults
Early return if (!valid) return; Guard clause โ€” exits function immediately

🧠 Test Yourself

What value does a function return when it has no return statement?





โ–ถ Try It Yourself