Functions

April 14, 2026
#javascript #js #functions #scope #closures

Functions are the building blocks of JavaScript programs. They let you wrap up a piece of logic, give it a name, and reuse it whenever you need it. In the Introduction to JavaScript, we wrote a few simple functions. This tutorial goes much deeper — we’ll cover how JavaScript handles scope, what closures are, and how to use functions as values (higher-order functions).

Function Declarations vs Expressions

There are two main ways to define a function. They look similar but behave differently.

// Function declaration — hoisted (available before the line it's defined on)
function greet(name) {
    return `Hello, ${name}!`;
}

// Function expression — NOT hoisted
const greetExpr = function (name) {
    return `Hello, ${name}!`;
};

console.log(greet("Alice"));     // "Hello, Alice!"
console.log(greetExpr("Bob"));   // "Hello, Bob!"

Hoisting means you can call a function declaration before it appears in your code. Function expressions must be defined before they’re used:

sayHi(); // works — declaration is hoisted
// sayBye(); // ReferenceError — expression is not hoisted

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

const sayBye = function () {
    console.log("Bye!");
};

Arrow Functions

Arrow functions are a shorter syntax introduced in ES6. They’re especially useful for short, inline functions.

// Regular function
const double = function (n) {
    return n * 2;
};

// Arrow function
const doubleArrow = (n) => {
    return n * 2;
};

// Implicit return (single expression — no braces, no return keyword)
const doubleShort = (n) => n * 2;

console.log(doubleShort(5)); // 10

Parentheses are optional when there’s exactly one parameter:

const square = n => n * n;
console.log(square(4)); // 16

Parameters and Arguments

Default Parameters

You can give parameters default values that kick in when no argument is passed:

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

console.log(createUser("Alice", "admin")); // { name: "Alice", role: "admin" }
console.log(createUser("Bob"));            // { name: "Bob", role: "viewer" }

Rest Parameters

The rest syntax (...) collects any number of arguments into an array:

function sum(...numbers) {
    return numbers.reduce((total, n) => total + n, 0);
}

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

Destructuring Parameters

You can destructure objects right in the parameter list — this is very common in real-world code:

function printUser({ name, age, role = "guest" }) {
    console.log(`${name} (${age}) — ${role}`);
}

printUser({ name: "Alice", age: 25, role: "admin" });
// "Alice (25) — admin"

printUser({ name: "Bob", age: 30 });
// "Bob (30) — guest"

Scope

Scope determines where a variable is accessible. JavaScript has three levels of scope.

Global Scope

Variables declared outside any function or block are global — accessible everywhere:

const appName = "MyApp";

function showApp() {
    console.log(appName); // accessible here
}

showApp(); // "MyApp"

Function Scope

Variables declared inside a function are only accessible within that function:

function calculate() {
    const result = 42;
    console.log(result); // 42
}

calculate();
// console.log(result); // ReferenceError — not accessible outside

Block Scope

let and const are scoped to the nearest {} block:

if (true) {
    const message = "inside block";
    console.log(message); // "inside block"
}

// console.log(message); // ReferenceError

Scope Chain

When JavaScript looks up a variable, it starts in the current scope and works outward until it finds it:

const color = "blue";

function outer() {
    const size = "large";

    function inner() {
        const shape = "circle";
        console.log(shape, size, color); // all accessible
    }

    inner();
}

outer(); // "circle large blue"

inner can access its own variables, outer’s variables, and global variables. This chain is the foundation of closures.

Closures

A closure is a function that remembers the variables from the scope where it was created, even after that scope has finished executing.

function createCounter() {
    let count = 0;

    return function () {
        count++;
        return count;
    };
}

const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3

createCounter has already returned, but the inner function still has access to count. That’s a closure — the inner function “closes over” the variable.

Here’s a practical example — a function that creates a greeting function for a specific language:

function greeter(language) {
    const greetings = {
        en: "Hello",
        es: "Hola",
        fr: "Bonjour",
    };

    return function (name) {
        return `${greetings[language]}, ${name}!`;
    };
}

const greetInSpanish = greeter("es");
const greetInFrench = greeter("fr");

console.log(greetInSpanish("Alice")); // "Hola, Alice!"
console.log(greetInFrench("Bob"));    // "Bonjour, Bob!"

Each returned function remembers its own language value.

Higher-Order Functions

A higher-order function is a function that takes another function as an argument or returns a function. You’ve already seen both patterns — Array.prototype.reduce takes a function, and createCounter returns one.

Functions as Arguments

This is the most common pattern. JavaScript’s built-in array methods rely on it heavily:

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

const doubled = numbers.map(n => n * 2);
console.log(doubled); // [2, 4, 6, 8, 10]

const evens = numbers.filter(n => n % 2 === 0);
console.log(evens); // [2, 4]

const sum = numbers.reduce((total, n) => total + n, 0);
console.log(sum); // 15

Writing Your Own

function repeat(n, action) {
    for (let i = 0; i < n; i++) {
        action(i);
    }
}

repeat(3, (i) => console.log(`Iteration ${i}`));
// "Iteration 0"
// "Iteration 1"
// "Iteration 2"

Immediately Invoked Function Expressions (IIFE)

An IIFE is a function that runs as soon as it’s defined. It’s useful for creating a private scope:

const result = (function () {
    const secret = 42;
    return secret * 2;
})();

console.log(result); // 84
// console.log(secret); // ReferenceError — not accessible

IIFEs were essential before let and const existed (since var doesn’t have block scope). You’ll still see them in older codebases and in some module patterns.

Putting It Together: A Mini Task Manager

Let’s combine closures, higher-order functions, and destructuring into a practical example:

function createTaskManager() {
    const tasks = [];

    return {
        add(title) {
            tasks.push({ title, done: false });
        },

        complete(title) {
            const task = tasks.find(t => t.title === title);
            if (task) task.done = true;
        },

        pending() {
            return tasks.filter(t => !t.done).map(t => t.title);
        },

        summary() {
            const done = tasks.filter(t => t.done).length;
            return `${done}/${tasks.length} tasks completed`;
        },
    };
}

const manager = createTaskManager();
manager.add("Write tutorial");
manager.add("Review code");
manager.add("Deploy site");
manager.complete("Write tutorial");

console.log(manager.pending());  // ["Review code", "Deploy site"]
console.log(manager.summary());  // "1/3 tasks completed"

The tasks array is private — it can only be accessed through the returned methods. This is the closure pattern in action, giving you encapsulation without classes.

What’s Next

Now that you understand how functions work in depth — scope, closures, and higher-order functions — you’re ready to tackle arrays, where you’ll use these patterns constantly with methods like map, filter, and reduce.

Thanks for visiting
We are actively updating content to this site. Thanks for visiting! Please bookmark this page and visit again soon.
Sponsor