Functions: Declarations, Expressions & Arrow
Functions: Declarations, Expressions & Arrow Functions
Functions are the single most important concept in JavaScript. Everything in modern JS — React components, event handlers, API calls, async operations — is built on functions. Understanding every form of function and when to use each is what separates junior from senior developers.
Three Ways to Write a Function
// 1. Function Declaration
function greet(name) {
return `Hello, ${name}!`;
}
// 2. Function Expression
const greet = function(name) {
return `Hello, ${name}!`;
};
// 3. Arrow Function (ES6+)
const greet = (name) => `Hello, ${name}!`;
// All three work the same way for basic calls:
console.log(greet("Alice")); // "Hello, Alice!"
They're not identical — and the differences matter.
Function Declarations vs Expressions: Hoisting
// You can call a function declaration BEFORE it's defined
sayHello(); // "Hello!" — works because of hoisting
function sayHello() {
console.log("Hello!");
}
// Function expressions are NOT hoisted
sayBye(); // TypeError: sayBye is not a function
const sayBye = function() {
console.log("Bye!");
};
Hoisting means function declarations are moved to the top of their scope before execution. Function expressions behave like variables — they must be defined before use.
Practical rule: Use const + arrow functions as your default. Reserve function declarations for top-level functions you want to call from anywhere in the file.
Arrow Functions — The Modern Default
// No parameters — use empty parens
const sayHi = () => "Hi!";
// One parameter — parens optional
const double = x => x * 2;
// Multiple parameters — parens required
const add = (a, b) => a + b;
// Multi-line body — requires curly braces AND explicit return
const processUser = (user) => {
const name = user.name.trim();
const email = user.email.toLowerCase();
return { name, email };
};
Key difference — this binding:
Arrow functions do NOT have their own this. They inherit this from the surrounding scope. This is critical in React and event-driven code:
// Problem with regular function
class Timer {
constructor() {
this.seconds = 0;
}
start() {
setInterval(function() {
this.seconds++; // BUG: 'this' is undefined or window
}, 1000);
}
}
// Fixed with arrow function
class Timer {
constructor() {
this.seconds = 0;
}
start() {
setInterval(() => {
this.seconds++; // 'this' is the Timer instance ✓
}, 1000);
}
}
Parameters
Default Parameters
function createUser(name, role = "user", active = true) {
return { name, role, active };
}
createUser("Alice") // { name: "Alice", role: "user", active: true }
createUser("Bob", "admin") // { name: "Bob", role: "admin", active: true }
createUser("Carol", "mod", false) // { name: "Carol", role: "mod", active: false }
Rest Parameters (...args)
// Collect remaining arguments into an array
function sum(...numbers) {
return numbers.reduce((total, n) => total + n, 0);
}
sum(1, 2, 3) // 6
sum(10, 20, 30, 40) // 100
// Combine named + rest
function logMessage(level, ...messages) {
console.log(`[${level}]`, messages.join(" "));
}
logMessage("ERROR", "File", "not", "found") // [ERROR] File not found
Functions as First-Class Citizens
In JavaScript, functions are values. You can:
// Assign to variables (already seen this)
const add = (a, b) => a + b;
// Pass as arguments (callbacks)
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(n => n * 2); // [2, 4, 6, 8, 10]
const evens = numbers.filter(n => n % 2 === 0); // [2, 4]
const total = numbers.reduce((sum, n) => sum + n, 0); // 15
// Return from functions (higher-order functions)
function createMultiplier(factor) {
return (n) => n * factor;
}
const triple = createMultiplier(3);
const tenX = createMultiplier(10);
console.log(triple(7)); // 21
console.log(tenX(5)); // 50
Closures — Functions That Remember
A closure is a function that retains access to variables from its outer scope, even after that outer function has finished executing:
function createCounter(start = 0) {
let count = start; // This variable is "closed over"
return {
increment() { count++; },
decrement() { count--; },
value() { return count; }
};
}
const counter = createCounter(10);
counter.increment();
counter.increment();
counter.decrement();
console.log(counter.value()); // 11
// 'count' is private — can't access it directly
console.log(counter.count); // undefined
Closures power module patterns, React hooks, and many design patterns.
Pure Functions vs Side Effects
// Pure function — same input always gives same output, no side effects
function add(a, b) {
return a + b;
}
// Impure — has side effects (modifies external state)
let total = 0;
function addToTotal(n) {
total += n; // Side effect: modifies outer variable
return total;
}
// Impure — depends on external state
function getCurrentUser() {
return window.currentUser; // Output depends on external state
}
Prefer pure functions — they're easier to test, debug, and reason about. This is core to React's model.
IIFE (Immediately Invoked Function Expression)
// Creates a private scope — used in older code to avoid polluting global scope
const result = (function() {
const privateData = "secret";
return {
getData: () => privateData
};
})();
result.getData() // "secret"
Less common with modern modules, but you'll see it in legacy codebases.
The Difference That Matters
| Hoisted | this | Use When | |
|---|---|---|---|
| Function declaration | Yes | Own this | Top-level named functions |
| Function expression | No | Own this | Named callback or method |
| Arrow function | No | Inherited | Default for almost everything |
Next lesson: Arrow Functions & this Binding — the deep dive into JavaScript's most confusing concept.
Get this course's notes on Telegram!
Free cheat sheets, summaries & practice exercises