🧩 Function Declaration and Expressions
Functions are the verbs of JavaScript — the reusable units of behavior every program is built from. In this lesson you'll learn the four ways to create a function, why some can be called before they're written and others can't, and how each style handles the tricky this keyword.
🎯 Learning Objectives
By the end of this lesson, you will be able to:
- Write and call function declarations and explain how hoisting lets you use them early
- Create function expressions and describe why they are not hoisted
- Use arrow functions for concise callbacks and explain their lexical
thisbinding - Recognize and apply the IIFE pattern to create a private scope
- Choose the right function style for a given situation
Estimated Time: 30–40 minutes • Difficulty: Beginner–Intermediate
Hands-on: Build a small calculator module that mixes all four function styles.
In This Lesson
What Is a Function?
A function is a named, reusable block of code that performs a task. You define it once and call (invoke) it as many times as you like, optionally feeding it different inputs each time. Functions are how you avoid copy-pasting logic and how you break a big problem into small, testable pieces.
💡 The recipe analogy: A function is like a recipe in a cookbook. It has a name so you can find it, a list of ingredients it needs (parameters), instructions to follow (the body), and it produces a finished dish (the return value). Once written, you can cook it again and again without rewriting a word.
Why bother wrapping code in functions at all? Because they buy you five things at once:
- Reusability — write the logic once, call it everywhere.
- Modularity — break a complex problem into small, named steps.
- Abstraction — hide messy details behind a simple name like
validateEmail(). - Maintainability — fix a bug in one place instead of ten.
- Testability — each function can be checked in isolation.
JavaScript gives you several syntaxes for creating a function. They look different and behave differently in a couple of important ways — but they all produce that same reusable block of behavior. Let's meet them one at a time.
Function Declarations & Hoisting
A function declaration (also called a function statement) begins with the function keyword followed by a name. It is the most classic and readable way to define a function.
function greet(name) {
return `Hello, ${name}!`;
}
console.log(greet('Alice')); // "Hello, Alice!"
The special power of a declaration is hoisting. Before your code runs, JavaScript scans each scope and moves every function declaration to the top. That means you can call the function above the line where it's written:
// This works — the declaration is hoisted to the top of the scope
console.log(calculateArea(5, 10)); // 50
function calculateArea(width, height) {
return width * height;
}
📖 Key Terms
Declaration: the code that defines a function.
Invocation / call: running the function with ().
Hoisting: JavaScript's behavior of registering declarations at the top of their scope before execution begins.
A realistic example: form validation
Declarations shine for the top-level, reusable helpers that form the backbone of a feature. Here, several small validators combine into one:
function validateEmail(email) {
const pattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return pattern.test(email);
}
function validatePassword(password) {
return password.length >= 8 && /\d/.test(password);
}
function validateForm(email, password) {
if (!validateEmail(email)) return 'Please enter a valid email address';
if (!validatePassword(password)) return 'Password needs 8+ characters and a number';
return 'Form is valid!';
}
console.log(validateForm('user@example.com', 'password123')); // Form is valid!
console.log(validateForm('not-an-email', 'password123')); // Please enter a valid email address
💡 When to reach for a declaration
Use declarations for the main, reusable functions of your program — especially when you value hoisting and want the structure of your file to be obvious at a glance.
Function Expressions
A function expression defines a function as part of a larger expression — most often by assigning it to a variable. Because the function is now a value stored in a variable, the ordinary rules of variable declaration apply: it is not hoisted the way a declaration is.
const add = function (a, b) {
return a + b;
};
console.log(add(5, 3)); // 8
Try to call it too early and you get an error, because the const binding hasn't been initialized yet:
// console.log(subtract(10, 5)); // ReferenceError: Cannot access 'subtract' before initialization
const subtract = function (a, b) {
return a - b;
};
console.log(subtract(10, 5)); // 5
Function expressions are usually anonymous (the function itself has no name), but you can give the function a name — a named function expression — which is handy for recursion and clearer stack traces:
// Anonymous function expression
const sayHello = function () {
console.log('Hello, world!');
};
// Named function expression — the name is only visible inside the function
const factorial = function calcFactorial(n) {
if (n <= 1) return 1;
return n * calcFactorial(n - 1); // recurse using the internal name
};
console.log(factorial(5)); // 120
Because they are values, function expressions are the natural choice when you need to pass a function to another function — for example, an event handler or an array callback:
document.getElementById('saveBtn').addEventListener('click', function (event) {
event.preventDefault();
console.log('Saved!');
});
⚠️ Declaration vs. expression, at a glance
Both create functions. The difference that bites beginners is timing: a declaration is available anywhere in its scope (hoisted); a function expression only exists after the line that assigns it runs.
Arrow Functions
Introduced in ES6 (2015), arrow functions are a shorter syntax for function expressions. They drop the function keyword in favor of a => "fat arrow," and they can implicitly return a single expression without curly braces or the return keyword.
// Implicit return — no braces, no `return`
const double = x => x * 2;
// Multiple parameters need parentheses
const multiply = (a, b) => a * b;
// A block body needs an explicit `return`
const divide = (a, b) => {
if (b === 0) throw new Error('Cannot divide by zero');
return a / b;
};
console.log(double(4)); // 8
console.log(multiply(3, 5)); // 15
console.log(divide(10, 2)); // 5
The conciseness is most valuable with array methods, where a callback is passed inline:
const numbers = [1, 2, 3, 4, 5];
// Traditional function expression
const squares1 = numbers.map(function (num) { return num * num; });
// Arrow function — same result, far less noise
const squares2 = numbers.map(num => num * num);
console.log(squares2); // [1, 4, 9, 16, 25]
The big difference: lexical this
Arrow functions do not have their own this. Instead they capture this from the surrounding (lexical) scope where they were written. This solves one of JavaScript's most notorious bugs — losing this inside a callback.
'this' set by HOW it is called"] --> C{Called as a method?
With new?
Standalone?} B["Arrow function:
'this' inherited from
the enclosing scope"] --> D[Always the same 'this'
as the code around it]
const counter = {
count: 0,
// ❌ Regular function callback loses `this`
startBroken() {
setTimeout(function () {
this.count++; // `this` is NOT counter here
console.log(this.count); // NaN
}, 1000);
},
// ✅ Arrow callback keeps the surrounding `this`
startFixed() {
setTimeout(() => {
this.count++; // `this` is counter
console.log(this.count); // 1
}, 1000);
}
};
counter.startFixed(); // 1 after one second
⚠️ When NOT to use an arrow function
- Object methods that need their own
this(an arrow method'sthisis the surrounding scope, usually not the object). - Constructors — arrow functions cannot be called with
new. - When you need the classic
argumentsobject (use rest parameters...argsinstead). - Generator functions that use
yield.
IIFEs & Private Scope
An Immediately Invoked Function Expression (IIFE, pronounced "iffy") is a function that runs the instant it's defined. You wrap the function in parentheses to turn it into an expression, then call it with ():
(function () {
const x = 10;
const y = 20;
console.log(x + y); // 30 — runs immediately
})();
console.log(typeof x); // "undefined" — x never leaked to the outer scope
Historically IIFEs were the main way to create a private scope and keep variables out of the global namespace. Combined with a returned object, they produce the classic module pattern with private state:
const counter = (function () {
let count = 0; // private — nothing outside can touch it
return {
increment() { return ++count; },
decrement() { return --count; },
getValue() { return count; }
};
})();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.getValue()); // 2
console.log(counter.count); // undefined — truly private
✅ The modern alternative: ES modules
Today, native ES modules give every file its own scope automatically, so most IIFE use-cases are handled by simple import/export. You'll still see IIFEs in older code and bundled scripts, so it's worth recognizing.
// counter.js
let count = 0;
export function increment() { return ++count; }
export function getValue() { return count; }
// main.js
import { increment, getValue } from './counter.js';
increment();
console.log(getValue()); // 1
Choosing a Style
Here's how the four styles compare on the features that actually change your program's behavior:
| Feature | Declaration | Expression | Arrow | IIFE |
|---|---|---|---|---|
| Hoisted | Yes | No | No | Runs immediately |
Own this | Yes | Yes | No (inherits) | Yes |
arguments object | Yes | Yes | No | Yes |
Usable with new | Yes | Yes | No | Not practical |
| Conciseness | Medium | Medium | High | Low |
A quick decision guide for everyday coding:
needing its own this?} F -->|Yes| G[Method / Function Expression] F -->|No| H{Need a one-off private scope?} H -->|Yes| I[IIFE] H -->|No| E
Hands-on Exercise
🏋️ Build a Calculator Module
Objective: Combine all four function styles into one small, self-contained module.
Instructions:
- Create a
Calculatorusing an IIFE so its helpers are private. - Inside, write
validateNumbersas a declaration. - Write
addandsubtractas function expressions. - Write
multiplyanddivideas arrow functions (guard against divide-by-zero). - Return a public object exposing the four operations.
💡 Hint
The outer IIFE returns an object literal. Only the properties you put on that object are public; every const and function declared inside but not returned stays private.
✅ Sample solution
const Calculator = (function () {
// private helper — a declaration
function validateNumbers(...args) {
return args.every(n => typeof n === 'number' && !Number.isNaN(n));
}
// function expressions
const add = function (a, b) {
if (!validateNumbers(a, b)) throw new Error('Numbers only');
return a + b;
};
const subtract = function (a, b) {
if (!validateNumbers(a, b)) throw new Error('Numbers only');
return a - b;
};
// arrow functions
const multiply = (a, b) => {
if (!validateNumbers(a, b)) throw new Error('Numbers only');
return a * b;
};
const divide = (a, b) => {
if (!validateNumbers(a, b)) throw new Error('Numbers only');
if (b === 0) throw new Error('Cannot divide by zero');
return a / b;
};
return { add, subtract, multiply, divide }; // public API
})();
console.log(Calculator.add(5, 3)); // 8
console.log(Calculator.multiply(7, 6)); // 42
console.log(Calculator.divide(20, 4)); // 5
Extend it: add a chain() method that returns an object with add/subtract/… methods each returning this, so calls can be chained like Calculator.chain(10).add(5).multiply(2).value().
Best Practices
✅ Do
- Give functions descriptive verb-noun names like
fetchUserProfile, nothandleordoStuff. - Keep functions small and focused — one job each (the Single Responsibility Principle).
- Prefer arrow functions for short inline callbacks.
- Use early returns / guard clauses to avoid deep nesting.
⚠️ Avoid
- Arrow functions as object methods when you need
thisto be the object. - Giant functions that do validation, database work, and UI updates all at once — split them.
- Relying on hoisting to call a function pages before it's defined; it works, but it hurts readability.
Compare a tangled function with a refactored one:
// ❌ Does everything
function processUserData(user) {
if (!user.name) throw new Error('Name required');
database.update(user);
sendEmail(user.email, 'Welcome!');
updateUI(user);
}
// ✅ One job each, orchestrated by a thin coordinator
function validateUser(user) {
if (!user.name) throw new Error('Name required');
return user;
}
function processUserData(user) {
const valid = validateUser(user);
database.update(valid);
sendEmail(valid.email, 'Welcome!');
updateUI(valid);
}
Summary & Quiz
🎉 Key Takeaways
- Function declarations are named and hoisted — callable before they appear.
- Function expressions assign a function to a variable and are not hoisted.
- Arrow functions are concise, support implicit return, and use lexical
this. - IIFEs run immediately and create a private scope; modern code often uses ES modules instead.
🎯 Quick Quiz
Question 1: Which statement runs without error?
Question 2: Why does an arrow function fix the "lost this" problem inside a setTimeout callback?
Question 3: What is the main purpose of an IIFE?
📚 Further Reading
🚀 What's Next?
Now that you can create functions, the next lesson dives into how data flows in and out of them — parameters, arguments, and return values, including default and rest parameters and destructuring.