Skip to main content

Functions & Scope

Function Declarations vs Expressions vs Arrows

// Function declaration — hoisted, callable before definition
function add(a, b) {
return a + b;
}

// Function expression — not hoisted
const multiply = function(a, b) {
return a * b;
};

// Arrow function — concise, no own `this`
const divide = (a, b) => a / b;

// Arrow with body
const greet = (name) => {
const msg = `Hello, ${name}`;
return msg;
};

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

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

// Returning an object literal — wrap in parens
const makeUser = (name) => ({ name, createdAt: new Date() });

Default Parameters

function createPost(title, status = 'draft', tags = []) {
return { title, status, tags };
}

createPost('Hello World');
// { title: 'Hello World', status: 'draft', tags: [] }

createPost('Hello World', 'published', ['js', 'tutorial']);
// { title: 'Hello World', status: 'published', tags: ['js', 'tutorial'] }

Rest Parameters

// Collect remaining arguments into an array
function sum(...numbers) {
return numbers.reduce((acc, n) => acc + n, 0);
}

sum(1, 2, 3, 4, 5); // 15

// Combine with named params
function log(level, ...messages) {
console.log(`[${level}]`, ...messages);
}

log('ERROR', 'Something', 'went', 'wrong');
// [ERROR] Something went wrong

Scope

const global = 'I am global';  // accessible everywhere

function outer() {
const outerVar = 'outer';

function inner() {
const innerVar = 'inner';
console.log(outerVar); // can access outer scope
console.log(global); // can access global scope
}

// console.log(innerVar); // ReferenceError
}

Closures

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

function makeCounter(start = 0) {
let count = start; // this variable is "closed over"

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

const counter = makeCounter(10);
counter.increment(); // 11
counter.increment(); // 12
counter.decrement(); // 11
counter.value(); // 11
counter.reset();
counter.value(); // 10

Another classic example — factory functions:

function multiply(multiplier) {
return (number) => number * multiplier; // closes over `multiplier`
}

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

double(5); // 10
triple(5); // 15

Higher-Order Functions

Functions that take or return other functions:

// Takes a function as argument
function runTwice(fn) {
fn();
fn();
}

runTwice(() => console.log('hello'));

// Returns a function
function debounce(fn, delay) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}

const handleSearch = debounce((query) => fetchResults(query), 300);

The Call Stack

JavaScript is single-threaded — it executes one thing at a time using a call stack:

function c() { console.log('c'); }
function b() { c(); console.log('b'); }
function a() { b(); console.log('a'); }

a();
// Call stack progression:
// → a() pushed
// → b() pushed inside a
// → c() pushed inside b
// c logs, pops
// b logs, pops
// a logs, pops
// Output: c, b, a

Understanding the call stack helps debug stack overflow errors (infinite recursion) and understand async behavior.

IIFE (Immediately Invoked Function Expression)

Used to create a private scope — still appears in legacy code:

(function() {
const secret = 'not accessible outside';
console.log('runs immediately');
})();

// Modern equivalent: just use a block with let/const
{
const secret = 'not accessible outside';
}

Pure Functions

A pure function always returns the same output for the same input and has no side effects:

//  Impure — depends on external state, mutates input
let total = 0;
function addToTotal(amount) {
total += amount; // side effect
return total;
}

// Pure — predictable, testable
function add(a, b) {
return a + b;
}

// Impure — mutates the array
function addItem(arr, item) {
arr.push(item); // mutation
return arr;
}

// Pure — returns new array
function addItem(arr, item) {
return [...arr, item];
}

Prefer pure functions — they're easier to test, debug, and reason about.