Skip to main content

🧩 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 this binding
  • 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.

How hoisting reorders a function declaration The source code calls a function before defining it; during the creation phase JavaScript registers the whole declaration first, so the call succeeds at run time. What you write calculateArea(5, 10) function calculateArea() {…} How JS runs it function calculateArea() {…} calculateArea(5, 10) ✓ hoist
Figure 1 — A function declaration is fully hoisted, so a call can appear before the definition in your source file.

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.

flowchart TD A["Regular function:
'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's this is the surrounding scope, usually not the object).
  • Constructors — arrow functions cannot be called with new.
  • When you need the classic arguments object (use rest parameters ...args instead).
  • 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
HoistedYesNoNoRuns immediately
Own thisYesYesNo (inherits)Yes
arguments objectYesYesNoYes
Usable with newYesYesNoNot practical
ConcisenessMediumMediumHighLow

A quick decision guide for everyday coding:

flowchart TD A[Need a function] --> B{Use it before it is defined?} B -->|Yes| C[Function Declaration] B -->|No| D{Is it a short callback?} D -->|Yes| E[Arrow Function] D -->|No| F{Object or class method
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:

  1. Create a Calculator using an IIFE so its helpers are private.
  2. Inside, write validateNumbers as a declaration.
  3. Write add and subtract as function expressions.
  4. Write multiply and divide as arrow functions (guard against divide-by-zero).
  5. 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, not handle or doStuff.
  • 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 this to 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.