Function Declarations

A function is a reusable block of code that performs a specific task. In JavaScript, the most traditional way to define a function is with a function declaration:

// Function declaration
function greet(name) {
    console.log(`Hello, ${name}!`);
}

// Calling the function
greet("Alice");   // "Hello, Alice!"
greet("Bob");     // "Hello, Bob!"

// A function that performs a calculation
function add(a, b) {
    return a + b;
}

const result = add(3, 5);
console.log(result); // 8

Function declarations are hoisted, which means you can call them before they appear in your code:

// This works — declarations are hoisted
sayHello(); // "Hello!"

function sayHello() {
    console.log("Hello!");
}

Function Expressions

A function expression assigns a function to a variable. The function itself can be anonymous (no name) or named:

// Anonymous function expression
const greet = function(name) {
    console.log(`Hello, ${name}!`);
};

greet("Alice"); // "Hello, Alice!"

// Named function expression (useful for debugging/recursion)
const factorial = function fact(n) {
    if (n <= 1) return 1;
    return n * fact(n - 1);
};

console.log(factorial(5)); // 120

Unlike declarations, function expressions are not hoisted. You must define them before you use them:

// This will NOT work — expressions are not hoisted
// sayHi(); // TypeError: sayHi is not a function

const sayHi = function() {
    console.log("Hi!");
};

sayHi(); // "Hi!" — works after the definition

Arrow Functions

Arrow functions (introduced in ES6) provide a shorter syntax for writing functions. They are especially popular for callbacks and short operations:

// Standard function expression
const add = function(a, b) {
    return a + b;
};

// Arrow function equivalent
const addArrow = (a, b) => {
    return a + b;
};

// Concise body — if there's just one expression, you can omit {} and return
const addShort = (a, b) => a + b;

// Single parameter — parentheses are optional
const double = n => n * 2;

// No parameters — empty parentheses required
const getRandom = () => Math.random();

// Examples in action
console.log(addShort(3, 5)); // 8
console.log(double(4));      // 8
console.log(getRandom());    // 0.7234... (random)
💡
Arrow functions vs regular functions

Arrow functions are not just shorter syntax — they behave differently with this. Arrow functions do not have their own this binding; they inherit this from the surrounding scope. This makes them ideal for callbacks but unsuitable for object methods that need to reference the object via this. Regular functions get their own this based on how they are called.

// Arrow functions and this
const counter = {
    count: 0,

    // Regular function — 'this' refers to counter
    increment: function() {
        this.count++;
        console.log(this.count);
    },

    // Arrow function — 'this' is inherited (NOT counter!)
    // incrementArrow: () => {
    //     this.count++;  // 'this' is the outer scope, not counter
    // }
};

counter.increment(); // 1
counter.increment(); // 2

// Where arrow functions shine: callbacks
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(n => n * 2);
console.log(doubled); // [2, 4, 6, 8, 10]

Parameters and Default Values

Functions can accept parameters (inputs). JavaScript is flexible — you can call a function with fewer or more arguments than defined:

function greet(name, greeting) {
    console.log(`${greeting}, ${name}!`);
}

greet("Alice", "Hello");     // "Hello, Alice!"
greet("Bob");                // "undefined, Bob!" — missing args become undefined

// Default parameters (ES6)
function greetDefault(name, greeting = "Hello") {
    console.log(`${greeting}, ${name}!`);
}

greetDefault("Alice", "Hi"); // "Hi, Alice!"
greetDefault("Bob");         // "Hello, Bob!" — uses default

// Default values can be expressions
function createUser(name, role = "viewer", joinDate = new Date()) {
    return { name, role, joinDate };
}

console.log(createUser("Alice"));
// { name: "Alice", role: "viewer", joinDate: [current date] }
console.log(createUser("Bob", "admin"));
// { name: "Bob", role: "admin", joinDate: [current date] }

Rest Parameters

// Collect remaining arguments into an array
function sum(...numbers) {
    let total = 0;
    for (const num of numbers) {
        total += num;
    }
    return total;
}

console.log(sum(1, 2, 3));       // 6
console.log(sum(10, 20, 30, 40)); // 100

// Rest must be the last parameter
function logAll(first, ...rest) {
    console.log(`First: ${first}`);
    console.log(`Rest: ${rest}`);
}

logAll("a", "b", "c", "d");
// "First: a"
// "Rest: b,c,d"

Return Values

Functions send data back using return. A function without a return statement (or with an empty return) returns undefined.

// Returning a value
function multiply(a, b) {
    return a * b;
}

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

// Early return (guard clause pattern)
function divide(a, b) {
    if (b === 0) {
        return "Cannot divide by zero";
    }
    return a / b;
}

console.log(divide(10, 2)); // 5
console.log(divide(10, 0)); // "Cannot divide by zero"

// Returning objects (wrap in parentheses with arrow functions)
const makeUser = (name, age) => ({ name, age });
console.log(makeUser("Alice", 25)); // { name: "Alice", age: 25 }

// Functions without return give undefined
function doSomething() {
    console.log("Working...");
    // no return statement
}

const result = doSomething(); // prints "Working..."
console.log(result);          // undefined

Scope

Scope determines where variables are accessible. JavaScript has three types of scope:

Global Scope

// Variables declared outside any function or block are global
const appName = "MyApp";
let userCount = 0;

function showApp() {
    // Global variables are accessible everywhere
    console.log(appName);  // "MyApp"
    userCount++;
}

showApp();
console.log(userCount); // 1

Function (Local) Scope

function calculateTax(amount) {
    const taxRate = 0.2;  // local to this function
    const tax = amount * taxRate;
    return tax;
}

console.log(calculateTax(100)); // 20
// console.log(taxRate);  // ReferenceError: taxRate is not defined
// console.log(tax);      // ReferenceError: tax is not defined

Block Scope

// let and const are block-scoped (limited to { })
if (true) {
    const blockConst = "I'm block-scoped";
    let blockLet = "Me too";
    console.log(blockConst); // works
    console.log(blockLet);   // works
}
// console.log(blockConst); // ReferenceError
// console.log(blockLet);   // ReferenceError

// for loop scope
for (let i = 0; i < 3; i++) {
    // i only exists inside this loop
}
// console.log(i); // ReferenceError
⚠️
var ignores block scope — this is why we avoid it

Variables declared with var are function-scoped, not block-scoped. This means a var inside an if, for, or while block leaks out to the surrounding function. It also gets "hoisted" to the top of the function (the declaration moves up, but the assignment stays where it is), which can cause variables to be undefined unexpectedly.

// var hoisting demonstration
console.log(x); // undefined (not an error — var is hoisted)
var x = 5;
console.log(x); // 5

// What JavaScript actually sees:
// var x;           // declaration hoisted to top
// console.log(x);  // undefined
// x = 5;           // assignment stays here
// console.log(x);  // 5

// let/const do NOT allow this
// console.log(y); // ReferenceError: Cannot access 'y' before initialization
// let y = 5;

Closures

A closure is a function that remembers the variables from the scope where it was created, even after that scope has finished executing. This is one of JavaScript's most powerful features.

// Basic closure
function createGreeter(greeting) {
    // The inner function "closes over" the greeting variable
    return function(name) {
        console.log(`${greeting}, ${name}!`);
    };
}

const sayHello = createGreeter("Hello");
const sayHi = createGreeter("Hi");

sayHello("Alice"); // "Hello, Alice!"
sayHi("Bob");      // "Hi, Bob!"
// greeting is "remembered" even though createGreeter has returned

Practical Closure: Counter

function createCounter(start = 0) {
    let count = start;

    return {
        increment: () => ++count,
        decrement: () => --count,
        getCount: () => count,
        reset: () => { count = start; }
    };
}

const counter = createCounter(10);
console.log(counter.getCount());  // 10
counter.increment();
counter.increment();
console.log(counter.getCount());  // 12
counter.decrement();
console.log(counter.getCount());  // 11
counter.reset();
console.log(counter.getCount());  // 10

// count is private — there's no way to access it directly
// console.log(counter.count); // undefined

Closure Gotcha: Loops

// Common mistake with var in loops
for (var i = 0; i < 3; i++) {
    setTimeout(function() {
        console.log(i); // prints 3, 3, 3 (not 0, 1, 2)
    }, 100);
}
// var is function-scoped, so all callbacks share the same i

// Fix: use let (block-scoped — each iteration gets its own i)
for (let i = 0; i < 3; i++) {
    setTimeout(function() {
        console.log(i); // prints 0, 1, 2
    }, 100);
}

Callback Functions

A callback is a function passed as an argument to another function. Callbacks are fundamental to JavaScript — they're used for event handling, array methods, timers, and asynchronous operations.

// Passing a function as an argument
function doMath(a, b, operation) {
    return operation(a, b);
}

const sum = doMath(5, 3, (a, b) => a + b);
const product = doMath(5, 3, (a, b) => a * b);

console.log(sum);     // 8
console.log(product); // 15

Callbacks with Array Methods

const numbers = [1, 2, 3, 4, 5];

// forEach — run a function on each element
numbers.forEach(num => console.log(num * 2));
// 2, 4, 6, 8, 10

// map — transform each element (returns new array)
const doubled = numbers.map(num => num * 2);
console.log(doubled); // [2, 4, 6, 8, 10]

// filter — keep elements that pass a test
const evens = numbers.filter(num => num % 2 === 0);
console.log(evens); // [2, 4]

// find — get the first element that passes a test
const firstBig = numbers.find(num => num > 3);
console.log(firstBig); // 4

Callbacks with Timers

// setTimeout — run once after a delay (milliseconds)
setTimeout(() => {
    console.log("This runs after 2 seconds");
}, 2000);

// setInterval — run repeatedly at an interval
let count = 0;
const intervalId = setInterval(() => {
    count++;
    console.log(`Tick ${count}`);
    if (count >= 5) {
        clearInterval(intervalId); // stop after 5 ticks
    }
}, 1000);

Callbacks with Event Listeners

// In a browser environment:
// document.getElementById("myButton").addEventListener("click", function(event) {
//     console.log("Button clicked!");
//     console.log(event.target); // the element that was clicked
// });

// Arrow function version:
// document.getElementById("myButton").addEventListener("click", (event) => {
//     console.log("Button clicked!");
// });

Summary

  • Function declarations are hoisted — can be called before they appear in code
  • Function expressions assign functions to variables — not hoisted
  • Arrow functions provide concise syntax and inherit this from their scope
  • Default parameters let you set fallback values: (name = "World")
  • Rest parameters collect extra arguments: (...args)
  • Functions return undefined unless you explicitly return a value
  • Scope: global, function (local), and block (let/const inside { })
  • Closures remember variables from their creation scope — essential for data privacy and factory patterns
  • Callbacks are functions passed to other functions — used everywhere in JavaScript
🎉
Functions mastered!

You now understand how to write, organize, and compose functions in JavaScript. Next up: arrays and objects — the two most important data structures you will work with every day.