Skip to main content
JavaScript beginner Lesson 7 of 24

Functions in JavaScript

Master JavaScript functions — declarations, expressions, arrow functions, default and rest parameters, IIFEs, and first-class function patterns.

Functions are the core building block of JavaScript programs. They let you name a piece of logic, reuse it anywhere, and compose small pieces into larger ones. Understanding the different ways to define and use functions — and how each handles this — will save you from some of the language’s most common bugs.

Function Declarations

A function declaration uses the function keyword followed by a name. It is hoisted to the top of its scope, meaning JavaScript processes the declaration before any code runs. This lets you call the function anywhere in the same file, even above where it is written — which can be useful for keeping the “main” logic at the top of a file and helper functions below.

// Call before definition — works because of hoisting
console.log(add(2, 3)); // 5

function add(a, b) {
  return a + b;
}

Function Expressions

A function expression assigns an anonymous (or named) function to a variable. Unlike declarations, function expressions are not hoisted — the variable binding exists but holds undefined (or is in the TDZ for const/let) until the line is reached. Use function expressions when you want to be explicit that the function is a value, when you need to pass it conditionally, or when you want to prevent accidental calls before initialization.

const multiply = function (a, b) {
  return a * b;
};

console.log(multiply(4, 5)); // 20

// Named function expression — the name is useful in stack traces and for recursion
const factorial = function fact(n) {
  return n <= 1 ? 1 : n * fact(n - 1); // `fact` is only visible inside the function
};
console.log(factorial(5)); // 120

Arrow Functions and this Binding

Arrow functions (=>) are a concise syntax introduced in ES2015. They’re ideal for callbacks and short transformations. Their key behavioral difference from regular functions is that they do not have their own this — they inherit this from the surrounding lexical scope. This solves a very common bug where this inside a callback unexpectedly refers to the wrong object.

// Concise one-liner — no braces needed, value is implicitly returned
const square = x => x * x;
const greet  = name => `Hello, ${name}!`;

// Multi-statement body requires braces and an explicit return
const clamp = (value, min, max) => {
  if (value < min) return min;
  if (value > max) return max;
  return value;
};

console.log(square(6));         // 36
console.log(greet("Alice"));    // Hello, Alice!
console.log(clamp(15, 0, 10));  // 10

this in regular vs arrow functions

const timer = {
  label: "job",
  startRegular() {
    setTimeout(function () {
      // Regular function creates its own 'this' — loses the object context
      console.log(this.label); // undefined (strict mode) or window.label
    }, 100);
  },
  startArrow() {
    setTimeout(() => {
      // Arrow captures 'this' from startArrow — keeps the object context
      console.log(this.label); // "job"
    }, 100);
  },
};

timer.startArrow(); // "job"

Rule of thumb: use arrow functions inside class methods and object methods when you pass callbacks. Use regular functions for the method itself.

Default Parameters

Default parameter values eliminate boilerplate guard code like if (x === undefined) x = defaultValue. They’re evaluated each time the function is called (not once at definition time), which means you can even reference earlier parameters in later defaults.

function createUser(name, role = "viewer", active = true) {
  return { name, role, active };
}

console.log(createUser("Alice"));                    // { name: "Alice", role: "viewer", active: true }
console.log(createUser("Bob", "admin"));             // { name: "Bob",   role: "admin",  active: true }
console.log(createUser("Carol", undefined, false));  // role keeps default "viewer" — undefined triggers the default

Defaults can reference earlier parameters:

function makeRange(start, end = start + 9) {
  return { start, end };
}
console.log(makeRange(1));    // { start: 1, end: 10 }
console.log(makeRange(5, 8)); // { start: 5, end: 8  }

Rest Parameters

Rest parameters collect all remaining arguments into a real array, giving you a clean way to write functions that accept any number of inputs. They replace the old arguments object, which was array-like but not a real array and didn’t work in arrow functions at all. Only one rest parameter is allowed per function, and it must be the last parameter.

function sum(...numbers) {
  // `numbers` is a genuine Array — all array methods work on it
  return numbers.reduce((total, n) => total + n, 0);
}
console.log(sum(1, 2, 3, 4, 5)); // 15

function logWithPrefix(prefix, ...messages) {
  messages.forEach(msg => console.log(`[${prefix}] ${msg}`));
}
logWithPrefix("INFO", "Server started", "Listening on port 3000");
// [INFO] Server started
// [INFO] Listening on port 3000

Spread in Function Calls

The spread operator (...) is the mirror image of rest parameters: instead of collecting arguments into an array, it unpacks an array into individual arguments. This is useful when you have data in an array but need to pass it to a function that expects separate arguments.

const nums = [3, 1, 4, 1, 5, 9, 2, 6];

// Math.max expects individual arguments — spread unpacks the array
console.log(Math.max(...nums)); // 9

function formatDate(year, month, day) {
  return `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
}

const parts = [2024, 7, 4];
console.log(formatDate(...parts)); // "2024-07-04"

IIFE — Immediately Invoked Function Expression

An IIFE is a function that defines and calls itself in a single expression. Before ES modules and block-scoped variables existed, IIFEs were the standard way to create a private scope and avoid polluting the global namespace. You still encounter them in legacy code, and they remain useful for running async logic at the top level in environments that don’t support top-level await.

// Classic IIFE — creates its own scope, variables don't leak out
const result = (function () {
  const secret = 42; // not accessible outside
  return secret * 2;
})();

console.log(result); // 84
// console.log(secret); // ReferenceError — secret is contained

// Async IIFE — run top-level async code in older environments
(async () => {
  const data = await fetch("/api/config").then(r => r.json());
  console.log(data);
})();

Pure Functions

A pure function always returns the same output for the same inputs and causes no side effects — it doesn’t modify external state, write to the DOM, make network requests, or change its arguments. Pure functions are the easiest to test because they have no hidden dependencies, and the easiest to reason about because their behavior is entirely described by their inputs and output.

// Pure — deterministic, no side effects
function formatPrice(amount, currency = "USD") {
  return new Intl.NumberFormat("en-US", { style: "currency", currency }).format(amount);
}
console.log(formatPrice(1999.9));       // "$1,999.90"
console.log(formatPrice(49.99, "EUR")); // "€49.99"

// Impure — depends on external state, result changes without changing arguments
let taxRate = 0.08;
function calcTotal(price) {
  return price + price * taxRate; // reading external variable = side cause
}

First-Class Functions

In JavaScript, functions are values just like numbers or strings. This is called “first-class” status: you can store functions in variables, pass them as arguments, return them from other functions, and put them in arrays or objects. This capability is the foundation for higher-order functions, callbacks, and every functional programming pattern in JavaScript.

// Store in a variable (already seen)
const double = x => x * 2;

// Store in an object (method)
const math = {
  add: (a, b) => a + b,
  sub: (a, b) => a - b,
};

// Pass as argument — `applyTwice` is a higher-order function
function applyTwice(fn, value) {
  return fn(fn(value));
}
console.log(applyTwice(double, 3)); // 12

// Return from a function — creates a closure (factory pattern)
function multiplier(factor) {
  return value => value * factor; // `factor` is captured in the closure
}
const triple = multiplier(3);
const byTen  = multiplier(10);
console.log(triple(7)); // 21
console.log(byTen(5));  // 50

// Real-world: compose a pipeline of transformations
const pipeline = (...fns) => value => fns.reduce((v, fn) => fn(v), value);

const processInput = pipeline(
  s => s.trim(),
  s => s.toLowerCase(),
  s => s.replace(/\s+/g, "-"),
);
console.log(processInput("  Hello World  ")); // "hello-world"

Common Pitfalls

Forgetting to return in an arrow function with braces:

// Bug: braces require an explicit return statement
const double = x => { x * 2 }; // returns undefined!

// Fix 1: remove braces for a single expression (implicit return)
const double1 = x => x * 2;

// Fix 2: add explicit return inside braces
const double2 = x => { return x * 2; };

Using arrow functions as object methods:

const counter = {
  count: 0,
  // Wrong: arrow captures 'this' from the outer scope (likely window/undefined)
  increment: () => { this.count++; },
  // Correct: method shorthand gets its own 'this' bound to the object
  decrement() { this.count--; },
};

arguments object doesn’t exist in arrow functions:

function oldStyle() {
  console.log(arguments[0]); // works — arguments is available
}

const newStyle = () => {
  console.log(arguments[0]); // ReferenceError in strict mode
};

// Use rest parameters in arrow functions instead
const newStyleFixed = (...args) => console.log(args[0]);

Frequently Asked Questions

What is the difference between a function declaration and a function expression?
A function declaration is hoisted — you can call it before it appears in the file. A function expression (assigned to a variable) is not hoisted; calling it before the assignment throws a ReferenceError (let/const) or returns undefined (var).
When should I use an arrow function instead of a regular function?
Use arrow functions for callbacks, array methods, and any short anonymous function. Use regular functions when you need your own 'this' binding — e.g. object methods, event handlers that reference the element, or constructor functions.
What does 'first-class function' mean?
It means functions are values. You can assign them to variables, pass them as arguments, return them from other functions, and store them in arrays or objects — the same as any other value like a string or number.