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.
// 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.
// 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.
// 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 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
// 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
// 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
// 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.
// 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.
// 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
| Feature | Declaration | Expression | Arrow |
|---|---|---|---|
| Syntax | function f(){} | const f = function(){} | const f = () => {} |
| Hoisted | Yes (fully) | No | No |
Own this | Yes | Yes | No (lexical) |
arguments | Yes | Yes | No |
| Used as constructor | Yes | Yes | No |
| Implicit return | No | No | Yes (concise body) |
| Best for | Top-level reusable functions | Conditional or assigned functions | Callbacks and short functions |