JavaScript — Functions Deep Dive
Functions are the fundamental building blocks of JavaScript. They are reusable blocks of code that can accept inputs, perform logic, and return outputs. Unlike many other languages, JavaScript treats functions as first-class citizens — they can be assigned to variables, passed as arguments, returned from other functions, and stored in data structures.
JavaScript supports multiple function syntaxes — declarations, expressions, and arrow functions — each with distinct behavior regarding hoisting, this binding, and the arguments object. Understanding these differences is essential for choosing the right tool for each scenario.
Functions also serve as the foundation for advanced patterns: higher-order functions enable functional programming, closures enable data privacy and factory functions, and pure functions enable predictable, testable code. This chapter covers everything from syntax fundamentals to advanced composition patterns.
| 1 | // A function is a reusable block of code |
| 2 | // At its simplest: input → logic → output |
| 3 | |
| 4 | function greet(name) { |
| 5 | return `Hello, ${name}!`; |
| 6 | } |
| 7 | |
| 8 | console.log(greet("World")); // "Hello, World!" |
| 9 | |
| 10 | // Functions are values — assign, pass, return |
| 11 | const fn = greet; |
| 12 | console.log(fn("Alice")); // "Hello, Alice!" |
| 13 | |
| 14 | // Functions are objects — they have properties and methods |
| 15 | console.log(greet.name); // "greet" |
| 16 | console.log(greet.length); // 1 (number of parameters) |
JavaScript provides three primary ways to define functions. Each has distinct behavior regarding hoisting, binding, and syntax. Choosing the right form depends on your use case — declarations for named standalone functions, expressions for dynamic assignment, and arrows for concise callbacks and lexical this.
| Feature | Declaration | Expression | Arrow |
|---|---|---|---|
| Syntax | function name() {} | const f = function() {} | const f = () => {} |
| Hoisting | Full hoist | No (Temporal Dead Zone) | No (TDZ) |
| this Binding | Dynamic (call-site) | Dynamic (call-site) | Lexical (enclosing scope) |
| arguments | Yes | Yes | No |
| Constructor | Yes (new) | Yes (new) | No |
| Name property | Function name | Variable or anonymous | Variable name |
| 1 | // FUNCTION DECLARATION — hoisted, can be called before definition |
| 2 | const result = multiply(3, 4); // Works! Hoisting at play |
| 3 | console.log(result); // 12 |
| 4 | |
| 5 | function multiply(a, b) { |
| 6 | return a * b; |
| 7 | } |
| 8 | |
| 9 | // FUNCTION EXPRESSION — not hoisted, assigned to a variable |
| 10 | // divide(10, 2); // TypeError: Cannot access before initialization |
| 11 | |
| 12 | const divide = function(a, b) { |
| 13 | return a / b; |
| 14 | }; |
| 15 | console.log(divide(10, 2)); // 5 |
| 16 | |
| 17 | // Named function expression (useful for recursion and stack traces) |
| 18 | const factorial = function fact(n) { |
| 19 | return n <= 1 ? 1 : n * fact(n - 1); |
| 20 | }; |
| 21 | console.log(factorial(5)); // 120 |
| 22 | |
| 23 | // ARROW FUNCTION — concise, lexical this, no arguments |
| 24 | const square = (x) => x * x; |
| 25 | console.log(square(4)); // 16 |
| 26 | |
| 27 | // When to use which: |
| 28 | // - Declaration: Named, standalone functions that benefit from hoisting |
| 29 | // - Expression: Functions assigned conditionally or to object properties |
| 30 | // - Arrow: Short callbacks, array methods, preserving lexical this |
best practice
JavaScript offers flexible parameter handling. Default parameters handle missing arguments, rest parameters capture variable-length arguments as an array, and destructured parameters extract values from objects or arrays passed as arguments. Modern JavaScript heavily uses these features for cleaner, more expressive function signatures.
Default Parameters
| 1 | // DEFAULT PARAMETERS — values used when argument is undefined |
| 2 | function greet(name = "Guest") { |
| 3 | return `Hello, ${name}!`; |
| 4 | } |
| 5 | |
| 6 | console.log(greet("Alice")); // "Hello, Alice!" |
| 7 | console.log(greet()); // "Hello, Guest!" |
| 8 | console.log(greet(undefined)); // "Hello, Guest!" (undefined triggers default) |
| 9 | console.log(greet(null)); // "Hello, null!" (null does NOT trigger default) |
| 10 | |
| 11 | // Defaults can reference other parameters |
| 12 | function createUser(name, role = "user", isActive = true) { |
| 13 | return { name, role, isActive }; |
| 14 | } |
| 15 | |
| 16 | // Defaults can be expressions (evaluated at call time) |
| 17 | function getConfig(path = "./config.json", cache = loadCache()) { |
| 18 | // cache is only evaluated if not provided |
| 19 | } |
| 20 | |
| 21 | // Default parameter order: non-defaults first |
| 22 | function fetchData(url, method = "GET", timeout = 5000) { |
| 23 | // url required, method and timeout optional |
| 24 | } |
Rest Parameters
| 1 | // REST PARAMETERS — captures remaining args as an array |
| 2 | function sum(...numbers) { |
| 3 | return numbers.reduce((total, n) => total + n, 0); |
| 4 | } |
| 5 | |
| 6 | console.log(sum(1, 2, 3, 4, 5)); // 15 |
| 7 | console.log(sum(10, 20)); // 30 |
| 8 | |
| 9 | // Rest with named parameters |
| 10 | function log(level, ...messages) { |
| 11 | console.log(`[${level.toUpperCase()}]`, ...messages); |
| 12 | } |
| 13 | |
| 14 | log("info", "Server started", "Port: 3000"); |
| 15 | // [INFO] Server started Port: 3000 |
| 16 | |
| 17 | // Rest must be the LAST parameter |
| 18 | function createEvent(name, date, ...attendees) { |
| 19 | return { name, date, attendees }; |
| 20 | } |
| 21 | |
| 22 | const event = createEvent("Conference", "2026-07-15", "Alice", "Bob", "Charlie"); |
| 23 | console.log(event.attendees); // ["Alice", "Bob", "Charlie"] |
| 24 | |
| 25 | // Difference from arguments object: rest is a real Array |
| 26 | function restVsArguments(...args) { |
| 27 | console.log(Array.isArray(args)); // true — real Array |
| 28 | console.log(args.map(x => x * 2)); // works natively |
| 29 | } |
Destructured Parameters
| 1 | // DESTRUCTURED PARAMETERS — extract properties directly |
| 2 | function printUser({ name, age, role = "user" }) { |
| 3 | console.log(`${name} (${age}) — ${role}`); |
| 4 | } |
| 5 | |
| 6 | printUser({ name: "Alice", age: 30 }); // Alice (30) — user |
| 7 | printUser({ name: "Bob", age: 25, role: "admin" }); // Bob (25) — admin |
| 8 | |
| 9 | // Destructured arrays |
| 10 | function firstAndLast([first, ...rest]) { |
| 11 | return { first, last: rest.pop() }; |
| 12 | } |
| 13 | |
| 14 | console.log(firstAndLast([1, 2, 3, 4, 5])); // { first: 1, last: 5 } |
| 15 | |
| 16 | // Nested destructuring |
| 17 | function processOrder({ id, customer: { name, email }, items }) { |
| 18 | console.log(`Order #${id} for ${name} (${email})`); |
| 19 | console.log(`${items.length} items`); |
| 20 | } |
| 21 | |
| 22 | // Deep destructuring with defaults |
| 23 | function configure({ |
| 24 | host = "localhost", |
| 25 | port = 8080, |
| 26 | tls = true, |
| 27 | credentials: { username, password } = {} |
| 28 | } = {}) { |
| 29 | return { host, port, tls, username, password }; |
| 30 | } |
| 31 | |
| 32 | console.log(configure({ host: "example.com" })); |
| 33 | // { host: "example.com", port: 8080, tls: true, username: undefined, password: undefined } |
| 34 | |
| 35 | // Combination: destructured + rest |
| 36 | function splitConfig({ database, cache, ...rest }) { |
| 37 | return { database, cache, other: rest }; |
| 38 | } |
| 39 | |
| 40 | const cfg = splitConfig({ |
| 41 | database: "mydb", |
| 42 | cache: true, |
| 43 | port: 3000, |
| 44 | debug: false |
| 45 | }); |
| 46 | // { database: "mydb", cache: true, other: { port: 3000, debug: false } } |
info
Every non-arrow function has an implicit arguments object — an array-like object containing all passed arguments. It exists even if no parameters are declared. While rest parameters are preferred in modern code, understanding arguments is important for legacy code and understanding JavaScript's function mechanics.
| 1 | // The arguments object — array-like, NOT an Array |
| 2 | function logArgs() { |
| 3 | console.log(arguments); // [Arguments] { '0': 'a', '1': 'b', '2': 'c' } |
| 4 | console.log(arguments.length); // 3 |
| 5 | console.log(arguments[0]); // "a" |
| 6 | console.log(arguments[1]); // "b" |
| 7 | |
| 8 | // arguments is NOT an Array |
| 9 | console.log(Array.isArray(arguments)); // false |
| 10 | // arguments.map(x => x) // TypeError: arguments.map is not a function |
| 11 | } |
| 12 | |
| 13 | logArgs("a", "b", "c"); |
| 14 | |
| 15 | // Convert arguments to a real Array |
| 16 | function variadic() { |
| 17 | const args = Array.from(arguments); |
| 18 | // or: const args = [...arguments]; |
| 19 | // or: const args = Array.prototype.slice.call(arguments); |
| 20 | return args.map(x => x.toUpperCase()); |
| 21 | } |
| 22 | |
| 23 | console.log(variadic("a", "b", "c")); // ["A", "B", "C"] |
| 24 | |
| 25 | // Strict mode vs sloppy mode |
| 26 | // In strict mode, arguments does not track parameter changes |
| 27 | function strictMode(a) { |
| 28 | "use strict"; |
| 29 | a = 42; |
| 30 | console.log(arguments[0]); // still the original value |
| 31 | } |
| 32 | |
| 33 | // In sloppy mode, arguments tracks parameter changes |
| 34 | function sloppyMode(a) { |
| 35 | a = 42; |
| 36 | console.log(arguments[0]); // 42 (tracks changes) |
| 37 | } |
| 38 | |
| 39 | // arguments.callee — reference to the currently executing function |
| 40 | // (forbidden in strict mode, use named function expressions instead) |
warning
JavaScript treats functions as first-class citizens. This means functions can be assigned to variables, stored in data structures, passed as arguments to other functions, and returned from functions. This property enables the callback pattern — passing a function to be executed later — which is fundamental to asynchronous programming, event handling, and functional programming.
| 1 | // Functions are values — assign to variables |
| 2 | const add = (a, b) => a + b; |
| 3 | const subtract = (a, b) => a - b; |
| 4 | |
| 5 | // Functions in data structures |
| 6 | const operations = { |
| 7 | add: (a, b) => a + b, |
| 8 | subtract: (a, b) => a - b, |
| 9 | multiply: (a, b) => a * b |
| 10 | }; |
| 11 | console.log(operations.add(5, 3)); // 8 |
| 12 | |
| 13 | // Functions as arguments — CALLBACKS |
| 14 | function processUser(name, callback) { |
| 15 | const formatted = callback(name); |
| 16 | return `Processed: ${formatted}`; |
| 17 | } |
| 18 | |
| 19 | function toUpper(str) { return str.toUpperCase(); } |
| 20 | function toLower(str) { return str.toLowerCase(); } |
| 21 | |
| 22 | console.log(processUser("Alice", toUpper)); // "Processed: ALICE" |
| 23 | console.log(processUser("Alice", toLower)); // "Processed: alice" |
| 24 | |
| 25 | // Anonymous inline callbacks |
| 26 | console.log(processUser("Bob", (name) => name.split("").reverse().join(""))); |
| 27 | // "Processed: boB" |
| 28 | |
| 29 | // Callbacks in async operations |
| 30 | function fetchData(url, onSuccess, onError) { |
| 31 | setTimeout(() => { |
| 32 | try { |
| 33 | const data = { id: 1, name: "Sample Data" }; |
| 34 | onSuccess(data); |
| 35 | } catch (err) { |
| 36 | onError(err); |
| 37 | } |
| 38 | }, 1000); |
| 39 | } |
| 40 | |
| 41 | fetchData( |
| 42 | "/api/data", |
| 43 | (data) => console.log("Got data:", data), |
| 44 | (err) => console.error("Failed:", err) |
| 45 | ); |
| 46 | |
| 47 | // Callbacks in array methods — the most common use |
| 48 | const numbers = [1, 2, 3, 4, 5]; |
| 49 | const doubled = numbers.map(n => n * 2); |
| 50 | const evens = numbers.filter(n => n % 2 === 0); |
| 51 | const sum = numbers.reduce((acc, n) => acc + n, 0); |
pro tip
A higher-order function is a function that takes one or more functions as arguments, returns a function, or both. This pattern is central to functional programming and enables powerful abstractions like function composition, decorators, and factories.
| 1 | // HOF: takes a function as argument |
| 2 | function withLogging(fn) { |
| 3 | return function(...args) { |
| 4 | console.log(`Calling ${fn.name} with:`, args); |
| 5 | const result = fn(...args); |
| 6 | console.log(`Result:`, result); |
| 7 | return result; |
| 8 | }; |
| 9 | } |
| 10 | |
| 11 | const addWithLog = withLogging((a, b) => a + b); |
| 12 | addWithLog(3, 4); |
| 13 | // Calling (anonymous) with: [3, 4] |
| 14 | // Result: 7 |
| 15 | |
| 16 | // HOF: returns a function (factory pattern) |
| 17 | function createMultiplier(factor) { |
| 18 | return (x) => x * factor; |
| 19 | } |
| 20 | |
| 21 | const double = createMultiplier(2); |
| 22 | const triple = createMultiplier(3); |
| 23 | console.log(double(5)); // 10 |
| 24 | console.log(triple(5)); // 15 |
| 25 | |
| 26 | // HOF: both takes and returns a function |
| 27 | function compose(f, g) { |
| 28 | return function(x) { |
| 29 | return f(g(x)); |
| 30 | }; |
| 31 | } |
| 32 | |
| 33 | const add1 = x => x + 1; |
| 34 | const times2 = x => x * 2; |
| 35 | const add1ThenTimes2 = compose(times2, add1); |
| 36 | console.log(add1ThenTimes2(5)); // 12 — (5 + 1) * 2 |
| 37 | |
| 38 | // Practical: timeout wrapper |
| 39 | function withTimeout(fn, ms = 5000) { |
| 40 | return function(...args) { |
| 41 | return Promise.race([ |
| 42 | fn(...args), |
| 43 | new Promise((_, reject) => |
| 44 | setTimeout(() => reject(new Error("Timeout")), ms) |
| 45 | ) |
| 46 | ]); |
| 47 | }; |
| 48 | } |
info
A pure function is a function that, given the same input, always returns the same output and has no side effects. Side effects include modifying external state, making network requests, reading files, or logging to the console. Pure functions are predictable, testable, and easier to reason about.
| Property | Pure Function | Impure Function |
|---|---|---|
| Deterministic | Same input → same output always | May differ per call |
| No side effects | Does not modify external state | May mutate arguments, globals, DOM |
| Referential transparency | Can replace call with its result | Cannot safely substitute |
| Testability | Trivial — no mocks needed | Requires setup, mocks, cleanup |
| Parallelism | Safe to run in parallel | Race conditions possible |
| 1 | // PURE — predictable, testable, safe |
| 2 | function add(a, b) { |
| 3 | return a + b; |
| 4 | } |
| 5 | |
| 6 | console.log(add(2, 3)); // 5 — always |
| 7 | console.log(add(2, 3)); // 5 — always |
| 8 | |
| 9 | // IMPURE — depends on external state, side effects |
| 10 | let counter = 0; |
| 11 | function increment() { |
| 12 | counter++; // side effect: modifies external state |
| 13 | return counter; |
| 14 | } |
| 15 | |
| 16 | // IMPURE — different result for same input |
| 17 | function getRandom() { |
| 18 | return Math.random(); // non-deterministic |
| 19 | } |
| 20 | |
| 21 | // IMPURE — modifies input (mutation) |
| 22 | function addItemImmutable(arr, item) { |
| 23 | arr.push(item); // side effect: mutates the original array |
| 24 | return arr; |
| 25 | } |
| 26 | |
| 27 | // PURE — returns new array, no mutation |
| 28 | function addItemPure(arr, item) { |
| 29 | return [...arr, item]; |
| 30 | } |
| 31 | |
| 32 | const original = [1, 2, 3]; |
| 33 | console.log(addItemPure(original, 4)); // [1, 2, 3, 4] |
| 34 | console.log(original); // [1, 2, 3] — unchanged |
| 35 | |
| 36 | // Practical: make impure code pure by injecting dependencies |
| 37 | // Impure: |
| 38 | function getUser(id) { |
| 39 | return fetch(`/api/users/${id}`) // side effect: network call |
| 40 | .then(r => r.json()); |
| 41 | } |
| 42 | |
| 43 | // Pure (dependency injection): |
| 44 | function getUserPure(id, apiClient) { |
| 45 | return apiClient.getUser(id); // side effect pushed to boundary |
| 46 | } |
best practice
Recursion occurs when a function calls itself to solve a smaller instance of the same problem. Every recursive function needs a base case (termination condition) and a recursive case (the self-call). While iterative solutions are often more performant in JavaScript due to call stack limits, recursion provides elegant solutions for tree traversal, divide-and-conquer algorithms, and problems with recursive structure.
| 1 | // BASIC RECURSION — factorial |
| 2 | function factorial(n) { |
| 3 | if (n <= 1) return 1; // base case |
| 4 | return n * factorial(n - 1); // recursive case |
| 5 | } |
| 6 | |
| 7 | console.log(factorial(5)); // 120 |
| 8 | |
| 9 | // Fibonacci — classic recursion |
| 10 | function fibonacci(n) { |
| 11 | if (n <= 1) return n; |
| 12 | return fibonacci(n - 1) + fibonacci(n - 2); |
| 13 | } |
| 14 | // Note: O(2^n) — use memoization for efficiency |
| 15 | |
| 16 | // Tail recursion — recursive call is the LAST operation |
| 17 | function factorialTail(n, accumulator = 1) { |
| 18 | if (n <= 1) return accumulator; |
| 19 | return factorialTail(n - 1, n * accumulator); // tail call |
| 20 | } |
| 21 | // Some engines optimize tail calls (TCO) |
| 22 | |
| 23 | // TREE TRAVERSAL — where recursion shines |
| 24 | const tree = { |
| 25 | value: 1, |
| 26 | children: [ |
| 27 | { value: 2, children: [{ value: 4, children: [] }, { value: 5, children: [] }] }, |
| 28 | { value: 3, children: [{ value: 6, children: [] }] } |
| 29 | ] |
| 30 | }; |
| 31 | |
| 32 | function traverseDFS(node, depth = 0) { |
| 33 | console.log(`${" ".repeat(depth)}Node: ${node.value}`); |
| 34 | for (const child of node.children) { |
| 35 | traverseDFS(child, depth + 1); |
| 36 | } |
| 37 | } |
| 38 | |
| 39 | traverseDFS(tree); |
| 40 | // Node: 1 |
| 41 | // Node: 2 |
| 42 | // Node: 4 |
| 43 | // Node: 5 |
| 44 | // Node: 3 |
| 45 | // Node: 6 |
| 46 | |
| 47 | // Recursion with accumulator (iterative-style recursion) |
| 48 | function deepFlatten(arr, result = []) { |
| 49 | for (const item of arr) { |
| 50 | if (Array.isArray(item)) { |
| 51 | deepFlatten(item, result); |
| 52 | } else { |
| 53 | result.push(item); |
| 54 | } |
| 55 | } |
| 56 | return result; |
| 57 | } |
| 58 | |
| 59 | console.log(deepFlatten([1, [2, [3, 4], 5], 6])); // [1, 2, 3, 4, 5, 6] |
| 60 | |
| 61 | // Convert recursion to iteration using explicit stack |
| 62 | function traverseDFSIterative(root) { |
| 63 | const stack = [{ node: root, depth: 0 }]; |
| 64 | while (stack.length > 0) { |
| 65 | const { node, depth } = stack.pop(); |
| 66 | console.log(`${" ".repeat(depth)}Node: ${node.value}`); |
| 67 | for (const child of [...node.children].reverse()) { |
| 68 | stack.push({ node: child, depth: depth + 1 }); |
| 69 | } |
| 70 | } |
| 71 | } |
warning
Function composition combines two or more functions to produce a new function. The output of one function becomes the input of the next. Composition is a core concept in functional programming — it allows building complex operations from simple, focused functions.
| 1 | // SIMPLE COMPOSITION — two functions |
| 2 | const compose = (f, g) => (x) => f(g(x)); |
| 3 | |
| 4 | const toUpper = (s) => s.toUpperCase(); |
| 5 | const exclaim = (s) => `${s}!`; |
| 6 | const shout = compose(exclaim, toUpper); |
| 7 | |
| 8 | console.log(shout("hello")); // "HELLO!" |
| 9 | |
| 10 | // COMPOSE — right-to-left (mathematical composition) |
| 11 | function composeMany(...fns) { |
| 12 | return (x) => fns.reduceRight((acc, fn) => fn(acc), x); |
| 13 | } |
| 14 | |
| 15 | const add1 = (x) => x + 1; |
| 16 | const times2 = (x) => x * 2; |
| 17 | const toString = (x) => `Result: ${x}`; |
| 18 | |
| 19 | const process = composeMany(toString, times2, add1); |
| 20 | console.log(process(5)); // "Result: 12" — (5 + 1) * 2, then toString |
| 21 | |
| 22 | // PIPE — left-to-right (pipeline, more intuitive) |
| 23 | function pipe(...fns) { |
| 24 | return (x) => fns.reduce((acc, fn) => fn(acc), x); |
| 25 | } |
| 26 | |
| 27 | const pipeline = pipe(add1, times2, toString); |
| 28 | console.log(pipeline(5)); // "Result: 12" — same result, different order |
| 29 | |
| 30 | // Practical: data transformation pipeline |
| 31 | const users = [ |
| 32 | { name: "Alice", age: 30, active: true }, |
| 33 | { name: "Bob", age: 17, active: true }, |
| 34 | { name: "Charlie", age: 25, active: false }, |
| 35 | { name: "Diana", age: 35, active: true } |
| 36 | ]; |
| 37 | |
| 38 | const getActiveUsers = (users) => users.filter(u => u.active); |
| 39 | const getNames = (users) => users.map(u => u.name); |
| 40 | const toUpperCase = (names) => names.map(n => n.toUpperCase()); |
| 41 | const sortNames = (names) => [...names].sort(); |
| 42 | |
| 43 | const getActiveUserNames = pipe( |
| 44 | getActiveUsers, |
| 45 | getNames, |
| 46 | toUpperCase, |
| 47 | sortNames |
| 48 | ); |
| 49 | |
| 50 | console.log(getActiveUserNames(users)); // ["ALICE", "BOB", "DIANA"] |
| 51 | |
| 52 | // Compose with binary/unary functions using partial application |
| 53 | const filter = (predicate) => (arr) => arr.filter(predicate); |
| 54 | const map = (transform) => (arr) => arr.map(transform); |
| 55 | const reduce = (reducer, initial) => (arr) => arr.reduce(reducer, initial); |
| 56 | |
| 57 | const sumOfSquares = pipe( |
| 58 | filter(n => n % 2 === 0), // keep evens |
| 59 | map(n => n * n), // square them |
| 60 | reduce((a, b) => a + b, 0) // sum |
| 61 | ); |
| 62 | |
| 63 | console.log(sumOfSquares([1, 2, 3, 4, 5])); // 4 + 16 = 20 |
info