Functions Basics

Difficulty: Beginner

Functions are one of the most important building blocks in JavaScript. A function is a reusable block of code designed to perform a specific task. You define a function once and can call it as many times as needed, with different inputs each time. Functions help you organize code into logical units, avoid repetition, and build abstractions that make complex programs manageable.

JavaScript offers three main ways to create functions. A function declaration uses the function keyword followed by a name: function greet(name) { ... }. Declarations are hoisted, meaning they can be called before the line where they are defined in the code. A function expression assigns an anonymous (or named) function to a variable: const greet = function(name) { ... }. Expressions are NOT hoisted - you must define them before calling them. Arrow functions (=>) provide a shorter syntax: const greet = (name) => { ... }. They also have different this binding behavior, which becomes important in object-oriented code.

Parameters are the names listed in the function definition; arguments are the actual values passed when calling the function. JavaScript is very flexible with arguments - you can pass fewer arguments than parameters (missing ones become undefined) or more arguments than parameters (extra ones are ignored, unless you use the rest parameter syntax). Default parameters, introduced in ES6, let you specify fallback values: function greet(name = "World") means if no name is passed, "World" is used.

The rest parameter syntax (...args) collects all remaining arguments into a real array. Unlike the legacy arguments object (which is array-like but not a real array), rest parameters give you a proper Array with all array methods available. Rest parameters must be the last parameter in the function definition. They are especially useful for functions that accept a variable number of arguments, like a sum function or a logging utility.

Every function in JavaScript returns a value. If you don't explicitly use a return statement, or if you write return without a value, the function returns undefined. The return statement immediately exits the function - any code after return in the same block is unreachable. Arrow functions with a single expression can use an implicit return by omitting the curly braces: const double = (x) => x * 2. This concise syntax is popular for short callback functions in array methods.

Functions create their own scope. Variables declared inside a function with let or const are not accessible outside that function. However, functions can access variables from their enclosing (outer) scope - this is the foundation of closures, a powerful pattern you'll explore in depth in a later lesson.

Code examples

Function Declaration vs Expression vs Arrow

// Function Declaration (hoisted)
console.log(add(2, 3)); // Works! Declarations are hoisted
function add(a, b) {
  return a + b;
}

// Function Expression (NOT hoisted)
const subtract = function(a, b) {
  return a - b;
};
console.log(subtract(10, 4));

// Arrow Function (concise syntax)
const multiply = (a, b) => a * b;
console.log(multiply(5, 3));

// Arrow with single param (parens optional)
const double = x => x * 2;
console.log(double(7));

// Arrow with function body
const divide = (a, b) => {
  if (b === 0) return "Cannot divide by zero";
  return a / b;
};
console.log(divide(10, 3));
console.log(divide(10, 0));

Declarations are hoisted (usable before definition). Expressions and arrows are NOT hoisted. Arrow functions with a single expression can omit curly braces and the return keyword for implicit return. With a function body (curly braces), you need an explicit return statement.

Parameters, Defaults, and Rest

// Default parameters
function greet(name = "World", greeting = "Hello") {
  return `${greeting}, ${name}!`;
}
console.log(greet());              // Both defaults
console.log(greet("Alice"));       // Second default
console.log(greet("Alice", "Hi")); // No defaults

// Rest parameters (...)
function sum(...numbers) {
  let total = 0;
  for (const n of numbers) {
    total += n;
  }
  return total;
}
console.log(sum(1, 2, 3));
console.log(sum(10, 20, 30, 40));

// Rest with leading params
function log(level, ...messages) {
  console.log(`[${level.toUpperCase()}]`, ...messages);
}
log("info", "Server started", "on port 3000");
log("error", "Connection failed");

Default parameters kick in when the argument is undefined (not passed or explicitly undefined). Rest parameters (...) gather remaining arguments into a real array. The rest parameter must always be the last parameter in the function signature.

Return Values and Early Returns

// Functions without return give undefined
function sayHi(name) {
  console.log(`Hi, ${name}`);
  // No return statement
}
const result = sayHi("Bob");
console.log(result);

// Early return pattern (guard clauses)
function processAge(age) {
  if (typeof age !== "number") return "Invalid: not a number";
  if (age < 0) return "Invalid: negative age";
  if (age < 18) return "Minor";
  if (age < 65) return "Adult";
  return "Senior";
}
console.log(processAge("twenty"));
console.log(processAge(-5));
console.log(processAge(12));
console.log(processAge(35));
console.log(processAge(70));

// Returning objects from arrow functions
const makeUser = (name, age) => ({ name, age });
console.log(makeUser("Alice", 25));

Functions without a return statement return undefined. Early returns (guard clauses) handle edge cases first, keeping the main logic clean and left-aligned. When returning an object literal from an arrow function, wrap it in parentheses to avoid confusion with a function body.

Function Scope

// Variables inside a function are local
function createCounter() {
  let count = 0; // Local to this function
  count++;
  return count;
}
console.log(createCounter()); // 1
console.log(createCounter()); // 1 (new count each call)
// console.log(count); // ReferenceError: count is not defined

// Functions can access outer scope variables
const multiplier = 3;
function tripleIt(x) {
  return x * multiplier; // Accesses outer 'multiplier'
}
console.log(tripleIt(5));
console.log(tripleIt(10));

// Nested functions
function outer() {
  const message = "Hello from outer";
  function inner() {
    console.log(message); // Can access outer's variables
  }
  inner();
}
outer();

Each function call creates a new scope with fresh local variables. Functions can read variables from their enclosing scope (outer scope) - this is called lexical scoping. The concept of inner functions accessing outer variables is the foundation of closures.

Key points

Concepts covered

Function Declaration, Function Expression, Arrow Functions, Parameters, Default Parameters, Rest Parameters