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 }) |
const or let are NOT hoisted (they are in the TDZ until the assignment line), so calling them before their declaration throws a ReferenceError.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.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
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 |