Skip to main content

Functions and Scope

Functions are the primary building blocks of any JavaScript program. They let you wrap up a block of code, name it, and reuse it. Understanding scope — where variables are accessible — is equally important.

Function Declarations

A function declaration defines a named function that is hoisted to the top of its scope.

function greet(name) {
  return `Hello, ${name}!`;
}

console.log(greet("Sabaoon")); // "Hello, Sabaoon!"

Because declarations are hoisted, you can call them before they appear in your code:

console.log(add(2, 3)); // 5 — works due to hoisting

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

Function Expressions

A function expression assigns a function to a variable. It is not hoisted.

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

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

Arrow Functions

Arrow functions provide a shorter syntax and do not have their own this binding.

const square = (n) => n * n;
const sum = (a, b) => a + b;

// Multi-line arrow function
const formatUser = (user) => {
  const { name, age } = user;
  return `${name} is ${age} years old`;
};

console.log(square(4));    // 16
console.log(sum(3, 7));    // 10
console.log(formatUser({ name: "Alex", age: 28 })); // "Alex is 28 years old"

When to Use Each

StyleHoistedOwn thisBest For
DeclarationYesYesTop-level named functions
ExpressionNoYesCallbacks, conditionals
ArrowNoNoShort callbacks, array methods

Default Parameters

You can assign default values to function parameters.

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

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

Rest Parameters

The rest operator ... collects remaining arguments into an array.

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

console.log(total(1, 2, 3, 4)); // 10

Understanding Scope

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

Global Scope

Variables declared outside any function or block are globally accessible.

const appName = "MyApp"; // global

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

Function Scope

Variables declared with var inside a function are only accessible within that function.

function example() {
  var secret = 42;
  console.log(secret); // 42
}

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

Block Scope

Variables declared with let or const inside a block ({}) are only accessible within that block.

if (true) {
  let blockVar = "inside";
  const blockConst = "also inside";
  console.log(blockVar); // "inside"
}

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

This is why let and const are preferred over var — they give you more predictable scoping.

Closures

A closure is a function that remembers the variables from its outer scope even after the outer function has returned.

function createCounter() {
  let count = 0;

  return {
    increment() {
      count++;
      return count;
    },
    decrement() {
      count--;
      return count;
    },
    getCount() {
      return count;
    },
  };
}

const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.decrement()); // 1
console.log(counter.getCount());  // 1

Closures are used everywhere in JavaScript — event handlers, callbacks, module patterns, and React hooks all rely on them.

Higher-Order Functions

A higher-order function either takes a function as an argument or returns a function.

// Takes a function as argument
function applyOperation(a, b, operation) {
  return operation(a, b);
}

console.log(applyOperation(10, 5, (a, b) => a + b)); // 15
console.log(applyOperation(10, 5, (a, b) => a * b)); // 50

// Returns a function
function createMultiplier(factor) {
  return (number) => number * factor;
}

const double = createMultiplier(2);
const triple = createMultiplier(3);

console.log(double(5));  // 10
console.log(triple(5));  // 15

Immediately Invoked Function Expressions (IIFE)

An IIFE runs immediately after it is defined. It creates a private scope.

const result = (() => {
  const secret = "hidden";
  return secret.toUpperCase();
})();

console.log(result); // "HIDDEN"
// console.log(secret); // ReferenceError

IIFEs were essential before ES modules existed. Today they are less common but still useful for one-time initialization.

Practical Exercise

Build a simple task manager using closures:

function createTaskManager() {
  const tasks = [];

  return {
    add(title) {
      tasks.push({ title, done: false, id: tasks.length + 1 });
    },
    complete(id) {
      const task = tasks.find((t) => t.id === id);
      if (task) task.done = true;
    },
    list() {
      return tasks.map(
        (t) => `${t.done ? "[x]" : "[ ]"} ${t.id}. ${t.title}`
      );
    },
    pending() {
      return tasks.filter((t) => !t.done).length;
    },
  };
}

const manager = createTaskManager();
manager.add("Learn JavaScript");
manager.add("Build a project");
manager.complete(1);

console.log(manager.list());
// ["[x] 1. Learn JavaScript", "[ ] 2. Build a project"]
console.log(manager.pending()); // 1

Key Takeaways

  • Function declarations are hoisted; expressions and arrow functions are not.
  • Arrow functions do not have their own this — use them for callbacks and array methods.
  • let and const are block-scoped, giving you tighter control over variable visibility.
  • Closures let inner functions access outer variables after the outer function has returned.
  • Higher-order functions accept or return other functions, enabling powerful composition patterns.