|$ curl https://forge-ai.dev/api/markdown?path=docs/js/functions
$cat docs/javascript-—-functions-deep-dive.md
updated Recently·40 min read·published

JavaScript — Functions Deep Dive

JavaScriptFunctionsAdvancedIntermediate to Advanced
Introduction

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.

functions-intro.js
JavaScript
1// A function is a reusable block of code
2// At its simplest: input → logic → output
3
4function greet(name) {
5 return `Hello, ${name}!`;
6}
7
8console.log(greet("World")); // "Hello, World!"
9
10// Functions are values — assign, pass, return
11const fn = greet;
12console.log(fn("Alice")); // "Hello, Alice!"
13
14// Functions are objects — they have properties and methods
15console.log(greet.name); // "greet"
16console.log(greet.length); // 1 (number of parameters)
Declarations vs Expressions vs Arrow

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.

FeatureDeclarationExpressionArrow
Syntaxfunction name() {}const f = function() {}const f = () => {}
HoistingFull hoistNo (Temporal Dead Zone)No (TDZ)
this BindingDynamic (call-site)Dynamic (call-site)Lexical (enclosing scope)
argumentsYesYesNo
ConstructorYes (new)Yes (new)No
Name propertyFunction nameVariable or anonymousVariable name
function-forms.js
JavaScript
1// FUNCTION DECLARATION — hoisted, can be called before definition
2const result = multiply(3, 4); // Works! Hoisting at play
3console.log(result); // 12
4
5function 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
12const divide = function(a, b) {
13 return a / b;
14};
15console.log(divide(10, 2)); // 5
16
17// Named function expression (useful for recursion and stack traces)
18const factorial = function fact(n) {
19 return n <= 1 ? 1 : n * fact(n - 1);
20};
21console.log(factorial(5)); // 120
22
23// ARROW FUNCTION — concise, lexical this, no arguments
24const square = (x) => x * x;
25console.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

Prefer function declarations for top-level functions — they are hoisted, named in stack traces, and self-documenting. Use arrow functions for short callbacks, array method arguments, and when you need lexical this. Use function expressions when you need dynamic assignment or the arguments object.
Parameters — Default, Rest, Destructured

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

default-params.js
JavaScript
1// DEFAULT PARAMETERS — values used when argument is undefined
2function greet(name = "Guest") {
3 return `Hello, ${name}!`;
4}
5
6console.log(greet("Alice")); // "Hello, Alice!"
7console.log(greet()); // "Hello, Guest!"
8console.log(greet(undefined)); // "Hello, Guest!" (undefined triggers default)
9console.log(greet(null)); // "Hello, null!" (null does NOT trigger default)
10
11// Defaults can reference other parameters
12function createUser(name, role = "user", isActive = true) {
13 return { name, role, isActive };
14}
15
16// Defaults can be expressions (evaluated at call time)
17function getConfig(path = "./config.json", cache = loadCache()) {
18 // cache is only evaluated if not provided
19}
20
21// Default parameter order: non-defaults first
22function fetchData(url, method = "GET", timeout = 5000) {
23 // url required, method and timeout optional
24}

Rest Parameters

rest-params.js
JavaScript
1// REST PARAMETERS — captures remaining args as an array
2function sum(...numbers) {
3 return numbers.reduce((total, n) => total + n, 0);
4}
5
6console.log(sum(1, 2, 3, 4, 5)); // 15
7console.log(sum(10, 20)); // 30
8
9// Rest with named parameters
10function log(level, ...messages) {
11 console.log(`[${level.toUpperCase()}]`, ...messages);
12}
13
14log("info", "Server started", "Port: 3000");
15// [INFO] Server started Port: 3000
16
17// Rest must be the LAST parameter
18function createEvent(name, date, ...attendees) {
19 return { name, date, attendees };
20}
21
22const event = createEvent("Conference", "2026-07-15", "Alice", "Bob", "Charlie");
23console.log(event.attendees); // ["Alice", "Bob", "Charlie"]
24
25// Difference from arguments object: rest is a real Array
26function restVsArguments(...args) {
27 console.log(Array.isArray(args)); // true — real Array
28 console.log(args.map(x => x * 2)); // works natively
29}

Destructured Parameters

destructured-params.js
JavaScript
1// DESTRUCTURED PARAMETERS — extract properties directly
2function printUser({ name, age, role = "user" }) {
3 console.log(`${name} (${age}) — ${role}`);
4}
5
6printUser({ name: "Alice", age: 30 }); // Alice (30) — user
7printUser({ name: "Bob", age: 25, role: "admin" }); // Bob (25) — admin
8
9// Destructured arrays
10function firstAndLast([first, ...rest]) {
11 return { first, last: rest.pop() };
12}
13
14console.log(firstAndLast([1, 2, 3, 4, 5])); // { first: 1, last: 5 }
15
16// Nested destructuring
17function 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
23function configure({
24 host = "localhost",
25 port = 8080,
26 tls = true,
27 credentials: { username, password } = {}
28} = {}) {
29 return { host, port, tls, username, password };
30}
31
32console.log(configure({ host: "example.com" }));
33// { host: "example.com", port: 8080, tls: true, username: undefined, password: undefined }
34
35// Combination: destructured + rest
36function splitConfig({ database, cache, ...rest }) {
37 return { database, cache, other: rest };
38}
39
40const 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

Destructured parameters shine for functions with many optional configuration options. Instead of remembering positional arguments like createUser("Alice", undefined, undefined, true), pass an options object and destructure it with defaults. This pattern is called "named parameters" and is used extensively in modern APIs.
The Arguments Object

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.

arguments-object.js
JavaScript
1// The arguments object — array-like, NOT an Array
2function 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
13logArgs("a", "b", "c");
14
15// Convert arguments to a real Array
16function 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
23console.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
27function 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
34function 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

Avoid using arguments in new code. Use rest parameters (...args) instead — they are real Arrays with full method support, they work in arrow functions, and they make the function signature explicit. The arguments object is only useful when working with legacy code or older-style variadic functions.
First-Class Functions & Callbacks

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.

first-class-functions.js
JavaScript
1// Functions are values — assign to variables
2const add = (a, b) => a + b;
3const subtract = (a, b) => a - b;
4
5// Functions in data structures
6const operations = {
7 add: (a, b) => a + b,
8 subtract: (a, b) => a - b,
9 multiply: (a, b) => a * b
10};
11console.log(operations.add(5, 3)); // 8
12
13// Functions as arguments — CALLBACKS
14function processUser(name, callback) {
15 const formatted = callback(name);
16 return `Processed: ${formatted}`;
17}
18
19function toUpper(str) { return str.toUpperCase(); }
20function toLower(str) { return str.toLowerCase(); }
21
22console.log(processUser("Alice", toUpper)); // "Processed: ALICE"
23console.log(processUser("Alice", toLower)); // "Processed: alice"
24
25// Anonymous inline callbacks
26console.log(processUser("Bob", (name) => name.split("").reverse().join("")));
27// "Processed: boB"
28
29// Callbacks in async operations
30function 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
41fetchData(
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
48const numbers = [1, 2, 3, 4, 5];
49const doubled = numbers.map(n => n * 2);
50const evens = numbers.filter(n => n % 2 === 0);
51const sum = numbers.reduce((acc, n) => acc + n, 0);
🔥

pro tip

Callbacks are the foundation of JavaScript's asynchronous model. While Promises and async/await have largely replaced raw callbacks for async flow control, callbacks remain essential for synchronous operations like array methods (map, filter, reduce) and event listeners. Always handle errors in asynchronous callbacks.
Higher-Order Functions

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.

higher-order-functions.js
JavaScript
1// HOF: takes a function as argument
2function 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
11const addWithLog = withLogging((a, b) => a + b);
12addWithLog(3, 4);
13// Calling (anonymous) with: [3, 4]
14// Result: 7
15
16// HOF: returns a function (factory pattern)
17function createMultiplier(factor) {
18 return (x) => x * factor;
19}
20
21const double = createMultiplier(2);
22const triple = createMultiplier(3);
23console.log(double(5)); // 10
24console.log(triple(5)); // 15
25
26// HOF: both takes and returns a function
27function compose(f, g) {
28 return function(x) {
29 return f(g(x));
30 };
31}
32
33const add1 = x => x + 1;
34const times2 = x => x * 2;
35const add1ThenTimes2 = compose(times2, add1);
36console.log(add1ThenTimes2(5)); // 12 — (5 + 1) * 2
37
38// Practical: timeout wrapper
39function 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

Higher-order functions enable separation of concerns. The core function handles the business logic, while the HOF handles cross-cutting concerns like logging, timing, caching, or access control. This is the foundation of decorator patterns and aspect-oriented programming in JavaScript.
Pure Functions & Side Effects

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.

PropertyPure FunctionImpure Function
DeterministicSame input → same output alwaysMay differ per call
No side effectsDoes not modify external stateMay mutate arguments, globals, DOM
Referential transparencyCan replace call with its resultCannot safely substitute
TestabilityTrivial — no mocks neededRequires setup, mocks, cleanup
ParallelismSafe to run in parallelRace conditions possible
pure-functions.js
JavaScript
1// PURE — predictable, testable, safe
2function add(a, b) {
3 return a + b;
4}
5
6console.log(add(2, 3)); // 5 — always
7console.log(add(2, 3)); // 5 — always
8
9// IMPURE — depends on external state, side effects
10let counter = 0;
11function increment() {
12 counter++; // side effect: modifies external state
13 return counter;
14}
15
16// IMPURE — different result for same input
17function getRandom() {
18 return Math.random(); // non-deterministic
19}
20
21// IMPURE — modifies input (mutation)
22function 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
28function addItemPure(arr, item) {
29 return [...arr, item];
30}
31
32const original = [1, 2, 3];
33console.log(addItemPure(original, 4)); // [1, 2, 3, 4]
34console.log(original); // [1, 2, 3] — unchanged
35
36// Practical: make impure code pure by injecting dependencies
37// Impure:
38function getUser(id) {
39 return fetch(`/api/users/${id}`) // side effect: network call
40 .then(r => r.json());
41}
42
43// Pure (dependency injection):
44function getUserPure(id, apiClient) {
45 return apiClient.getUser(id); // side effect pushed to boundary
46}

best practice

Strive to write pure functions for business logic. Push side effects to the boundaries of your application — event handlers, framework lifecycle methods, or dedicated service layers. Pure functions are trivially testable (no mocks, no setup), cacheable (memoization), and composable. They are the foundation of predictable, maintainable code.
Recursion Patterns

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.

recursion.js
JavaScript
1// BASIC RECURSION — factorial
2function factorial(n) {
3 if (n <= 1) return 1; // base case
4 return n * factorial(n - 1); // recursive case
5}
6
7console.log(factorial(5)); // 120
8
9// Fibonacci — classic recursion
10function 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
17function 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
24const 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
32function 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
39traverseDFS(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)
48function 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
59console.log(deepFlatten([1, [2, [3, 4], 5], 6])); // [1, 2, 3, 4, 5, 6]
60
61// Convert recursion to iteration using explicit stack
62function 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

JavaScript engines have a maximum call stack size (~10,000 frames in most browsers). Deep recursion can cause a RangeError: Maximum call stack size exceeded. For deep traversals, use an explicit stack (iterative approach) or trampolining. Tail call optimization (TCO) is only available in strict mode and not in all engines.
Function Composition

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.

function-composition.js
JavaScript
1// SIMPLE COMPOSITION — two functions
2const compose = (f, g) => (x) => f(g(x));
3
4const toUpper = (s) => s.toUpperCase();
5const exclaim = (s) => `${s}!`;
6const shout = compose(exclaim, toUpper);
7
8console.log(shout("hello")); // "HELLO!"
9
10// COMPOSE — right-to-left (mathematical composition)
11function composeMany(...fns) {
12 return (x) => fns.reduceRight((acc, fn) => fn(acc), x);
13}
14
15const add1 = (x) => x + 1;
16const times2 = (x) => x * 2;
17const toString = (x) => `Result: ${x}`;
18
19const process = composeMany(toString, times2, add1);
20console.log(process(5)); // "Result: 12" — (5 + 1) * 2, then toString
21
22// PIPE — left-to-right (pipeline, more intuitive)
23function pipe(...fns) {
24 return (x) => fns.reduce((acc, fn) => fn(acc), x);
25}
26
27const pipeline = pipe(add1, times2, toString);
28console.log(pipeline(5)); // "Result: 12" — same result, different order
29
30// Practical: data transformation pipeline
31const 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
38const getActiveUsers = (users) => users.filter(u => u.active);
39const getNames = (users) => users.map(u => u.name);
40const toUpperCase = (names) => names.map(n => n.toUpperCase());
41const sortNames = (names) => [...names].sort();
42
43const getActiveUserNames = pipe(
44 getActiveUsers,
45 getNames,
46 toUpperCase,
47 sortNames
48);
49
50console.log(getActiveUserNames(users)); // ["ALICE", "BOB", "DIANA"]
51
52// Compose with binary/unary functions using partial application
53const filter = (predicate) => (arr) => arr.filter(predicate);
54const map = (transform) => (arr) => arr.map(transform);
55const reduce = (reducer, initial) => (arr) => arr.reduce(reducer, initial);
56
57const 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
63console.log(sumOfSquares([1, 2, 3, 4, 5])); // 4 + 16 = 20

info

Use pipe for left-to-right data flow (most intuitive) and compose for right-to-left mathematical composition. Each function in the pipeline should take one input and produce one output — this makes functions reusable and composable. Break complex logic into small, focused functions and compose them.
$Blueprint — Engineering Documentation·Section ID: JS-FUNCS·Revision: 1.0