Intermediate

Functions in JavaScript

Learn how to define and use functions in JavaScript - declarations, arrow functions, parameters, scope, closures, and higher-order functions with Docker-ready examples

Functions are the heart of JavaScript. As a multi-paradigm language with strong functional roots (it was originally meant to embed Scheme), JavaScript treats functions as first-class values - they can be stored in variables, passed as arguments, returned from other functions, and created on the fly. This is what makes patterns like callbacks, promises, and event handlers possible.

JavaScript offers several ways to define a function, and they are not interchangeable. Function declarations are hoisted and bring their own this; arrow functions are compact and inherit this from their surrounding scope. Understanding which to reach for is a core JavaScript skill.

In this tutorial you will learn how to declare functions, pass parameters (including default values and the rest operator), understand variable scope and closures, write recursive functions, and use higher-order functions - functions that operate on other functions.

Defining and Calling Functions

The most traditional way to define a function is the function declaration. JavaScript also supports function expressions (storing a function in a variable) and arrow functions (a concise ES6 syntax).

Create a file named functions_basics.js:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Function declaration - hoisted, can be called before it appears
function greet(name) {
  return `Hello, ${name}!`;
}

// Function expression - assigned to a const
const square = function (n) {
  return n * n;
};

// Arrow function - concise ES6 syntax
const cube = (n) => n * n * n;

// A function can return nothing (implicitly returns undefined)
function logSeparator() {
  console.log("----------");
}

console.log(greet("Ada"));
console.log("square(5) =", square(5));
console.log("cube(3) =", cube(3));
logSeparator();
console.log("Return of logSeparator:", logSeparator());

Function declarations are hoisted, meaning they are available throughout their scope even before the line where they are written. Function expressions and arrow functions assigned to const are not - you must define them before use.

Parameters: Defaults and Rest

JavaScript parameters are flexible. Any argument you omit is undefined, you can give parameters default values, and the rest parameter (...args) collects an arbitrary number of arguments into an array.

Create a file named functions_params.js:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// Default parameters fill in when an argument is missing
function createUser(name, role = "guest") {
  return `${name} (${role})`;
}

// Rest parameter gathers any number of arguments into an array
function sum(...numbers) {
  return numbers.reduce((total, n) => total + n, 0);
}

// Destructuring parameters with defaults
function buildUrl({ host, port = 80, path = "/" }) {
  return `http://${host}:${port}${path}`;
}

console.log(createUser("Grace"));
console.log(createUser("Linus", "admin"));
console.log("sum() =", sum());
console.log("sum(1, 2, 3, 4) =", sum(1, 2, 3, 4));
console.log(buildUrl({ host: "example.com" }));
console.log(buildUrl({ host: "example.com", port: 8080, path: "/api" }));

Scope and Closures

A closure is a function that “remembers” the variables from the scope in which it was created, even after that scope has finished executing. Closures are one of JavaScript’s most powerful features and the foundation of data privacy and stateful callbacks.

Create a file named functions_scope.js:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const globalLabel = "global";

function outer() {
  const localLabel = "local";
  // Inner function closes over localLabel and globalLabel
  function inner() {
    console.log(`inner sees: ${localLabel} and ${globalLabel}`);
  }
  inner();
}

// A closure that keeps private state
function makeCounter() {
  let count = 0; // private - not accessible from outside
  return function () {
    count += 1;
    return count;
  };
}

outer();

const counter = makeCounter();
console.log("count:", counter());
console.log("count:", counter());
console.log("count:", counter());

// A second counter has its own independent state
const other = makeCounter();
console.log("other count:", other());

The count variable lives on inside the returned function. Each call to makeCounter() produces a fresh, independent closure - which is why other starts back at 1.

Recursion

A function that calls itself is recursive. JavaScript supports recursion naturally; here is the classic factorial.

Create a file named functions_recursion.js:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// Factorial: n! = n * (n-1) * ... * 1
function factorial(n) {
  if (n <= 1) {
    return 1; // base case stops the recursion
  }
  return n * factorial(n - 1);
}

// Fibonacci sequence, also recursive
function fib(n) {
  if (n < 2) {
    return n;
  }
  return fib(n - 1) + fib(n - 2);
}

console.log("factorial(5) =", factorial(5));
console.log("factorial(0) =", factorial(0));
console.log("fib(10) =", fib(10));

Every recursive function needs a base case - a condition that stops the recursion - otherwise it would call itself forever and throw a “Maximum call stack size exceeded” error.

Higher-Order Functions

Because functions are first-class values, JavaScript can pass them around like any other data. A higher-order function either takes a function as an argument, returns a function, or both. The array methods map, filter, and reduce are everyday examples.

Create a file named functions_higher_order.js:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
const numbers = [1, 2, 3, 4, 5, 6];

// map: transform every element
const doubled = numbers.map((n) => n * 2);

// filter: keep elements that pass a test
const evens = numbers.filter((n) => n % 2 === 0);

// reduce: collapse a list into a single value
const total = numbers.reduce((acc, n) => acc + n, 0);

// A function that returns a function (a "factory")
function multiplier(factor) {
  return (n) => n * factor;
}

const triple = multiplier(3);

console.log("doubled:", doubled);
console.log("evens:", evens);
console.log("total:", total);
console.log("triple(7) =", triple(7));

Running with Docker

1
2
3
4
5
6
7
8
9
# Pull the official Node.js Alpine image
docker pull node:22-alpine

# Run each example
docker run --rm -v $(pwd):/app -w /app node:22-alpine node functions_basics.js
docker run --rm -v $(pwd):/app -w /app node:22-alpine node functions_params.js
docker run --rm -v $(pwd):/app -w /app node:22-alpine node functions_scope.js
docker run --rm -v $(pwd):/app -w /app node:22-alpine node functions_recursion.js
docker run --rm -v $(pwd):/app -w /app node:22-alpine node functions_higher_order.js

Expected Output

Running functions_basics.js:

Hello, Ada!
square(5) = 25
cube(3) = 27
----------
----------
Return of logSeparator: undefined

Running functions_params.js:

Grace (guest)
Linus (admin)
sum() = 0
sum(1, 2, 3, 4) = 10
http://example.com:80/
http://example.com:8080/api

Running functions_scope.js:

inner sees: local and global
count: 1
count: 2
count: 3
other count: 1

Running functions_recursion.js:

factorial(5) = 120
factorial(0) = 1
fib(10) = 55

Running functions_higher_order.js:

doubled: [ 2, 4, 6, 8, 10, 12 ]
evens: [ 2, 4, 6 ]
total: 21
triple(7) = 21

Key Concepts

  • Three ways to define functions - declarations (hoisted), expressions, and arrow functions; they differ in hoisting and how they handle this.
  • Functions are first-class values - they can be stored in variables, passed as arguments, and returned from other functions.
  • Default and rest parameters - param = value supplies fallbacks, while ...args collects a variable number of arguments into an array.
  • Closures capture their environment - an inner function retains access to outer variables even after the outer function returns, enabling private state.
  • Recursion needs a base case - without a terminating condition, recursive calls overflow the call stack.
  • Higher-order functions power functional JavaScript - map, filter, and reduce plus function factories let you compose behavior from small reusable pieces.
  • Arrow functions are concise but different - they inherit this from the surrounding scope, making them ideal for callbacks but unsuitable as object methods that rely on this.

Running Today

All examples can be run using Docker:

docker pull node:22-alpine
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining