JavaScript Functions — The Building Blocks
What is a Function?
A function is a named, reusable block of code that performs a specific task. You define it once and call it as many times as you need. Think of a function as a recipe: you write the steps once, give the recipe a name, then "cook" it whenever you want the result.
Functions follow the DRY principle — Don't Repeat Yourself. Instead of writing the same validation logic in ten places, you write it once as a function. When you need to fix a bug, you fix it in one place, and every call site benefits automatically.
this). All three are valid — the context determines which one to use.Function Declarations
The classic way to create a function uses the function keyword, followed by a name, parentheses for parameters, and a block body. The defining feature of declarations is hoisting — JavaScript moves the entire function definition to the top of its scope before any code runs. You can call a declared function before its definition in the source file.
// Hoisting demo: call before definition works! console.log(greet("Alice")); // Works! "Hello, Alice!" function greet(name) { return `Hello, ${name}!`; } // Multi-parameter function function add(a, b) { return a + b; } console.log(add(3, 7)); // 10 // Function with no return (returns undefined) function logWelcome(user) { console.log(`Welcome back, ${user}!`); } logWelcome("Bob"); // Welcome back, Bob!
Function Expressions
A function expression assigns an anonymous (or named) function to a variable. Unlike declarations, function expressions are not hoisted — you can only call them after the assignment line. They're frequently used when you need to pass functions as arguments, conditionally define logic, or control exactly when a function becomes available.
// Function expression — NOT hoisted const multiply = function(a, b) { return a * b; }; console.log(multiply(4, 6)); // 24 // Named function expression (useful in stack traces) const factorial = function calcFactorial(n) { if (n <= 1) return 1; return n * calcFactorial(n - 1); // recursion via internal name }; console.log(factorial(5)); // 120 // Conditionally define a function const debug = true; const log = debug ? function(msg) { console.log("[DEBUG]", msg); } : function(msg) { /* silent in production */ };
ReferenceError (with const/let) or returns undefined when called (with var, because only the variable is hoisted, not the value). Always define expressions before calling them.Arrow Functions (ES6)
Arrow functions, introduced in ES2015, provide a shorter syntax for writing function expressions. They use the => token instead of the function keyword. Beyond brevity, they have one important behavioral difference: they do not have their own this binding. They inherit this from the surrounding lexical scope, which makes them ideal for callbacks inside classes and objects.
// Full arrow function const add = (a, b) => { return a + b; }; // Implicit return (single expression — no braces, no return) const double = x => x * 2; console.log(double(7)); // 14 // No params: use empty parens const greet = () => "Hello, World!"; // Two params: always use parens const power = (base, exp) => base ** exp; console.log(power(2, 10)); // 1024 // Multi-line arrow (needs braces + explicit return) const clamp = (val, min, max) => { if (val < min) return min; if (val > max) return max; return val; }; console.log(clamp(150, 0, 100)); // 100
thisthis refers to the object that called it. In an arrow function, this is inherited from the enclosing scope (the code around it). This makes arrow functions perfect for callbacks inside class methods — they won't accidentally rebind this to a DOM element or event object.Parameters & Arguments
Parameters are the named placeholders in the function definition. Arguments are the actual values you pass when calling the function. JavaScript is flexible: you can pass too few arguments (extras are undefined) or use default values to make parameters optional. You can also collect any number of arguments with rest parameters.
// Default parameters (ES6) function greet(name = "World", emoji = "👋") { return `Hello, ${name}! ${emoji}`; } console.log(greet()); // Hello, World! 👋 console.log(greet("Alice")); // Hello, Alice! 👋 console.log(greet("Bob", "🎉")); // Hello, Bob! 🎉 // Rest parameters — collect extra args into array function sum(...nums) { return nums.reduce((acc, n) => acc + n, 0); } console.log(sum(1, 2, 3)); // 6 console.log(sum(10, 20, 30, 40)); // 100 // Mixed: required param + rest function tag(label, ...items) { return items.map(i => `[${label}] ${i}`); } console.log(tag("INFO", "Server started", "DB connected"));
... syntax does double duty. As a rest parameter in a function definition, it collects arguments into an array. As a spread operator in a function call, it expands an array into individual arguments. Same syntax, opposite directions: function f(...args) vs f(...myArray).Return Values
The return statement sends a value back to the caller and immediately exits the function. Without a return statement (or with a bare return), the function returns undefined. Every function has exactly one return value — but you can pack multiple values into an array or object and destructure them at the call site.
// Early return — guard clause pattern function divide(a, b) { if (b === 0) { console.warn("Cannot divide by zero"); return null; // early exit } return a / b; } console.log(divide(10, 2)); // 5 console.log(divide(10, 0)); // null (with warning) // Return multiple values as an object function getMinMax(arr) { return { min: Math.min(...arr), max: Math.max(...arr) }; } const { min, max } = getMinMax([3, 1, 8, 2, 7]); console.log(min, max); // 1 8
Closures
A closure is a function that remembers the variables from its outer scope even after that outer scope has finished executing. When you define a function inside another function, the inner function forms a closure over the outer function's variables — they stay alive as long as the inner function exists.
Closures are the foundation of private state, factory functions, and module patterns in JavaScript. They're also central to how React hooks and many other patterns work under the hood.
// Counter factory — each call creates independent state function makeCounter(start = 0) { let count = start; // private variable — not accessible outside return { increment() { return ++count; }, decrement() { return --count; }, reset() { count = start; return count; }, getValue() { return count; } }; } const counterA = makeCounter(0); const counterB = makeCounter(100); counterA.increment(); // 1 counterA.increment(); // 2 counterB.decrement(); // 99 console.log(counterA.getValue()); // 2 — independent of B console.log(counterB.getValue()); // 99 — independent of A // count is not directly accessible — true encapsulation! // console.log(count); // ReferenceError
count variable in the example above is completely private. You can only change it through the methods the factory provides. This is the same pattern used in React's useState, event listeners, and module systems.Higher-Order Functions
A higher-order function (HOF) is a function that accepts another function as an argument and/or returns a function. This is possible because in JavaScript, functions are first-class values — they can be stored in variables, passed around, and returned just like numbers or strings.
Higher-order functions are the foundation of array methods like map, filter, and reduce. They're also how event listeners, setTimeout, and callbacks work throughout the browser API.
// HOF: function that accepts a function as argument function applyTwice(fn, value) { return fn(fn(value)); } const triple = x => x * 3; console.log(applyTwice(triple, 2)); // 18 (triple(triple(2)) = triple(6) = 18) // HOF: function that returns a function (function factory) function multiplier(factor) { return (number) => number * factor; } const double = multiplier(2); const tenX = multiplier(10); console.log(double(5)); // 10 console.log(tenX(7)); // 70 // Built-in HOFs: setTimeout uses a callback setTimeout(() => { console.log("Runs after 1 second"); }, 1000); // Array HOFs (callbacks) const nums = [1, 2, 3, 4, 5]; const evens = nums.filter(n => n % 2 === 0); // [2, 4] const doubled = nums.map(n => n * 2); // [2,4,6,8,10] const total = nums.reduce((acc, n) => acc + n, 0); // 15
Function Type Comparison
| Feature | Declaration | Expression | Arrow |
|---|---|---|---|
| Hoisted? | Yes (fully) | No | No |
Own this? |
Yes | Yes | No (lexical) |
| Can be anonymous? | No | Yes | Yes |
| Implicit return? | No | No | Yes (single expr) |
| Best for | Named utilities, top-level | Conditional defs, assigned fns | Callbacks, short transforms |
this binding?const double = x => x * 2; — what does double(5) return?...args) collect?makeCounter function that uses a closure to maintain private state and returns an object with three methods:•
increment() — adds 1 to the counter and returns the new value•
decrement() — subtracts 1 from the counter and returns the new value•
getValue() — returns the current counter value without modifying itCreate two independent counters and verify they don't share state. Then extend it: add a
reset() method that returns the counter to its initial value.
Show hints
- Declare
let count = 0(or use an initial parameter) insidemakeCounter - Return a plain object with three (or four) method properties using shorthand:
{ increment() {...}, decrement() {...}, getValue() {...} } - Each method modifies or reads the
countvariable via closure — no external access is needed - Test independence:
const a = makeCounter(); const b = makeCounter(10); a.increment(); b.decrement();—a.getValue()andb.getValue()should return different values - For
reset(), save the initial value in a second variable:let initial = start, then setcount = initial