Lesson 3

Functions

Home › Courses › JavaScript › Lesson 3
🟨 Lesson 3 · 20 minutes

Functions.
Package logic. Reuse it everywhere.

A function is a named block of code you can call from anywhere. Instead of copy-pasting the same ten lines into five places, you write them once in a function and call it five times. This is the biggest jump in code quality you'll make in your first month. Once functions click, you stop writing scripts and start writing programs.

~20 minutes Beginner ES2024 · browser
JavaScript path ← Lesson 2 Declarations Arrow functions Parameters Return values Closures Challenge

A JavaScript function is defined three ways: function name() (declaration), const name = function() (expression), or const name = () => (arrow function). Arrow functions are shorter and inherit this from the enclosing scope — that's the whole difference. Return values come from return; without it, functions return undefined. Closures are what make callbacks possible.

Before this lesson: complete Lesson 2. You need to be comfortable with const, let, and arrow expressions.

📝 Three ways to declare a function

Declarations, expressions, and arrow functions. Learn all three, use arrow functions most.

JavaScript
// 1. Function declaration — hoisted, callable before definition
function greet(name) {
return "Hello, " + name + "!";
}
console.log(greet("Alice")); // "Hello, Alice!"

// 2. Function expression — assigned to a variable
const add = function(a, b) {
return a + b;
};
console.log(add(3, 5)); // 8

// 3. Arrow function — ES6, shorter syntax
const double = (x) => x * 2;
console.log(double(5)); // 10

// Arrow with multiple params
const multiply = (a, b) => a * b;
console.log(multiply(3, 4)); // 12

// Arrow with no params
const getRandom = () => Math.random();
console.log(getRandom());

// Arrow with a block body — needs explicit return
const subtract = (a, b) => {
const result = a - b;
return result;
};
console.log(subtract(10, 3)); // 7

Declaration

function name() {} — hoisted to the top of its scope, so you can call it before its definition. Useful for organizing a file with utilities at the bottom.

Expression

const name = function() {} — not hoisted. Can only be called after the line that defines it. Rarely used now that arrow functions exist.

Arrow function

const name = () => — the modern default. Shorter, no this binding of its own, perfect for callbacks and array methods.

When to use which

Arrow for most things. Function declarations for named helpers you want hoisted. Never use function expressions in new code — arrow is shorter.

The curly brace trap. const f = (x) => { x * 2 } returns undefined. The braces create a block, not an expression. To return a value from a block, you must use return: const f = (x) => { return x * 2; }. Or drop the braces entirely: const f = (x) => x * 2. This trips up everyone — you'll hit it in the first week.

➡️ Arrow functions — the modern default

Shorter, cleaner, and the way most modern JavaScript is written.

JavaScript
// Traditional function
function square1(x) {
return x * x;
}

// Arrow function — one param, no parens needed
const square2 = x => x * x;

// Arrow with parens (optional for one param, required for 0 or 2+)
const square3 = (x) => x * x;

// Return object literal — wrap in parens
const makeUser = (name) => ({ name: name, active: true });
console.log(makeUser("Alice")); // { name: "Alice", active: true }

// Multi-line arrow — must use return
const greet = (name) => {
const greeting = "Hello, " + name;
return greeting + "!";
};

// Arrow functions in array methods (the most common use)
const nums = [1, 2, 3, 4, 5];
const doubled = nums.map(n => n * 2);
const evens = nums.filter(n => n % 2 === 0);
const total = nums.reduce((sum, n) => sum + n, 0);

// Chain them
const result = nums
.filter(n => n > 2)
.map(n => n * 10)
.reduce((a, b) => a + b, 0);
console.log(result); // 120 (30 + 40 + 50)

The this difference. Regular functions have their own this — it depends on how the function is called. Arrow functions inherit this from the enclosing scope — they don't have their own. This is why arrow functions are perfect for callbacks (they don't mess up the outer this) and why they're wrong for object methods that need to access this. Rule: use arrow functions everywhere except when you need this to refer to the object itself.

When the parameter list has zero or more than one parameter, parens are required. x => x * 2 works. (x) => x * 2 also works. () => 42 requires parens. (a, b) => a + b requires parens. Most style guides always use parens for consistency.

🎛️ Parameters — arguments with defaults

Default values, rest parameters, and object destructuring make functions flexible.

JavaScript
// Default parameters — used when the caller doesn't pass a value
function greet(name = "World") {
return "Hello, " + name + "!";
}
console.log(greet()); // "Hello, World!"
console.log(greet("Alice")); // "Hello, Alice!"

// Defaults can reference earlier params
const createUser = (name, role = name === "admin" ? "admin" : "user") => ({ name, role });

// Rest parameters — collect extra args into an array
function sum(...numbers) {
return numbers.reduce((a, b) => a + b, 0);
}
console.log(sum(1, 2, 3)); // 6
console.log(sum(1, 2, 3, 4, 5)); // 15

// Spread operator — expand an array into arguments
const values = [1, 2, 3, 4];
console.log(sum(...values)); // 10

// Destructuring parameters — extract properties from an object
function createUserDestructured({ name, age, email = "none" }) {
return { name, age, email };
}
console.log(createUserDestructured({ name: "Alice", age: 30 }));
// { name: "Alice", age: 30, email: "none" }

// Destructuring with rename
const showUser = ({ name: userName, age }) => userName + " (" + age + ")";
console.log(showUser({ name: "Alice", age: 30 })); // "Alice (30)"

Default parameters

function f(x = 10) {} — if the caller doesn't pass x, it's 10. The default can be any expression.

Rest parameters

function f(...args) {} — collects all remaining arguments into an array called args. Must be the last parameter.

Spread

f(...arr) — expands an array into individual arguments. The opposite of rest. Same ... syntax, used in call sites instead of definitions.

Object destructuring

function f({ name, age }) {} — extracts name and age from the passed object. The caller passes { name: "A", age: 30 }. Cleaner than accessing properties inside the function.

↩️ Return values

What the function gives back to the caller. Without return, you get undefined.

JavaScript
// Implicit return — arrow function without braces
const double = x => x * 2;

// Explicit return — required with braces
const double2 = x => {
return x * 2;
};

// No return — returns undefined
const logIt = x => {
console.log(x);
// no return statement
};
console.log(logIt("hi")); // undefined

// Return exits the function immediately
function checkAge(age) {
if (age < 0) return "invalid";
if (age < 18) return "minor";
return "adult";
// anything here is unreachable
}
console.log(checkAge(-5)); // "invalid"
console.log(checkAge(15)); // "minor"
console.log(checkAge(30)); // "adult"

// Return multiple values via an object
function minMax(nums) {
return {
min: Math.min(...nums),
max: Math.max(...nums),
};
}
const { min, max } = minMax([3, 1, 4, 1, 5]);
console.log(min, max); // 1 5

// Return multiple values via an array
function minMaxArr(nums) {
return [Math.min(...nums), Math.max(...nums)];
}
const [lo, hi] = minMaxArr([3, 1, 4]);
console.log(lo, hi); // 1 4

Chaining doesn't work with return inside arrow without braces. [1,2,3].forEach(n => console.log(n)) returns undefined (forEach returns undefined anyway), but [1,2,3].map(n => { console.log(n); }) returns [undefined, undefined, undefined]. If you want the return value, either drop the braces or add return.

Pure vs impure functions. A pure function takes inputs and returns outputs without side effects — no console.log, no DOM changes, no mutation. Pure functions are easier to test, reason about, and reuse. const add = (a, b) => a + b is pure. const logAndAdd = (a, b) => { console.log(a); return a + b; } is impure because of the console.log. Aim for pure functions; isolate impure ones (logging, network, DOM) into specific places.

🔒 Closures — the concept that unlocks everything

Functions remember the scope they were defined in, even after that scope exits.

A closure is created when a function "captures" a variable from the scope around it. The function keeps a reference to that variable, even after the outer function has returned. This is why callbacks work, why event handlers can access the outer variables, and why JavaScript has a real concept of "state" without classes.

JavaScript
// A function that returns a function — creates a closure
function makeCounter() {
let count = 0;
return function() {
count += 1;
return count;
};
}

const counter1 = makeCounter();
const counter2 = makeCounter();

console.log(counter1()); // 1
console.log(counter1()); // 2
console.log(counter1()); // 3

console.log(counter2()); // 1 (separate closure)
console.log(counter2()); // 2

// What happened? counter1 and counter2 each captured their OWN count.
// Even though makeCounter() returned, count stayed alive in memory.

// Closure over parameters
function multiplier(factor) {
return x => x * factor;
}

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

console.log(double(5)); // 10
console.log(triple(5)); // 15
console.log(double(10)); // 20

// Practical use — a function that only runs once
function once(fn) {
let called = false;
return function(...args) {
if (called) return undefined;
called = true;
return fn(...args);
};
}

const init = once(() => console.log("initialized"));
init(); // "initialized"
init(); // (nothing — called is true)

// Practical use — a simple cache
function memoize(fn) {
const cache = {};
return function(n) {
if (n in cache) return cache[n];
const result = fn(n);
cache[n] = result;
return result;
};
}

const slowSquare = n => {
console.log("computing...");
return n * n;
};
const fastSquare = memoize(slowSquare);
console.log(fastSquare(5)); // "computing..." then 25
console.log(fastSquare(5)); // 25 (no computing — cached)

What is a closure?

A function that keeps access to its outer scope's variables, even after the outer function has returned. The inner function "closes over" those variables.

Why it matters

Closures enable callbacks, event handlers, currying, memoization, and modules. Nearly every advanced JavaScript pattern uses them. Understanding closures is the line between beginner and intermediate.

Each closure is independent

makeCounter() called twice creates two separate count variables. Each returned function has its own private state.

Memory implication

Closures keep captured variables alive in memory. That's how the counter remembers count — but it also means closures can accidentally hold onto large data. Usually not a problem; worth knowing.

The classic closure gotcha with var. In a loop with var, all closures share the same variable:

// Broken with var — all closures print 3
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Prints: 3 3 3

// Fixed with let — each iteration has its own i
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Prints: 0 1 2

This is why let exists. let creates a new binding for each loop iteration; var has one binding for the entire function. If you see this bug, change var to let.

🔍 What just happened

Why the code did what it did — the mental model.

1

Functions are values

You can store a function in a variable, pass it as an argument, or return it. This is what "first-class function" means. It's the reason .map(x => x * 2) works — you're passing a function as an argument.

2

Arrow functions inherit this

Inside a regular function, this depends on how the function is called. Inside an arrow function, this is whatever it was where the arrow was defined. Arrow functions capture the outer this — they don't have their own.

3

Closures keep captured variables alive

When a function returns another function, the returned function keeps a live reference to the outer function's variables. Those variables aren't garbage-collected while the inner function exists. That's the mechanism that makes the counter remember its count.

4

Return exits; return value is what the caller gets

When a function hits return, it stops executing and gives the value back to the caller. If there's no return, the function returns undefined. If the return is inside a conditional that doesn't run, the function falls through to the end and returns undefined.

🏆 Challenge

Three tasks. Each teaches a different function concept.

Task 1 — Arrow conversions

Convert these function declarations to arrow functions:

1. function square(x) { return x * x; }
2. function isEven(n) { return n % 2 === 0; }
3. function greet(name = "World") { return "Hello, " + name; }

Test each in the console to verify the arrow versions behave identically.

Task 2 — Chain array methods

Given const nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], write a single chain that:

1. Filters to keep only even numbers.
2. Maps each remaining number to its square.
3. Reduces the result to the sum.

Expected result: 4 + 16 + 36 + 64 + 100 = 220. One chain, one line (or three lines if you prefer to break it up).

Task 3 — Build a closure

Write a function makeGreeter(greeting) that returns a function taking a name. The returned function should print "greeting, name!". Example:

const helloGreeter = makeGreeter("Hello");
helloGreeter("Alice"); → "Hello, Alice!"
const hiGreeter = makeGreeter("Hi");
hiGreeter("Bob"); → "Hi, Bob!"

The returned function should "remember" the greeting parameter. That's a closure.

⚠️ Common mistakes

The function bugs that catch every beginner.

1. Forgetting return in an arrow with braces

const f = (x) => { x * 2 } returns undefined. With braces, you must use return: const f = (x) => { return x * 2; }. Or drop the braces.

2. Calling a function without parentheses

const x = greet stores the function itself. const x = greet() calls it and stores the return value. Always add () to execute.

3. Using arrow functions as object methods

Object methods need this to refer to the object. Arrow functions don't have their own this — they inherit it from the enclosing scope. Use a regular function for object methods.

4. Modifying parameters

Reassigning a parameter changes it only inside the function — the caller's variable isn't affected. If you need to change the input, return a new value instead.

5. The var-in-loop closure bug

for (var i = 0; i < 3; i++) setTimeout(() => console.log(i)) prints 3 3 3 because all closures share the same var. Use let — it creates a new binding per iteration.

6. Thinking functions are hoisted like variables

Function declarations hoist to the top of the scope — you can call them before definition. But const fn = () => {} does NOT hoist the same way. If you call fn() before its definition, you get Cannot access 'fn' before initialization.

7. Forgetting the parentheses in a default expression

function f(cb = myFn) {} uses the function itself. function f(cb = myFn()) {} calls it and uses the return value. Both are valid — know which one you want.

8. Not checking if a callback exists

function run(cb) { cb() } throws TypeError if cb is undefined. Fix: if (cb) cb() or cb?.(). Optional chaining makes it one character.

➡️ What's next

Functions unlock the DOM — the ability to make pages interactive.

❓ Frequently asked questions

What is the difference between a function declaration and a function expression?

A declaration uses the function keyword at statement level and hoists to the top of its scope — you can call it before its definition. An expression is assigned to a variable and only exists after the assignment. Modern practice: use function declarations for named helpers and arrow functions for everything else.

What is the difference between arrow functions and regular functions?

Arrow functions are shorter and inherit this from the enclosing scope. Regular functions have their own this, which depends on how they're called. Arrow functions can't be used as constructors (no new), and can't be used for object methods that need this. For 90% of code, use arrow functions.

What is a closure in JavaScript?

A closure is a function that keeps access to variables from its outer scope, even after the outer function has returned. Closures enable callbacks, event handlers, memoization, and module patterns. Every time you return a function from another function, you create a closure.

What is the difference between rest and spread?

Both use ... syntax but in different places. Rest is in the parameter list and collects arguments into an array: function f(...args). Spread is in the call site and expands an array into arguments: f(...arr). Same operator, different directions.

Why does my function return undefined?

Either the function has no return statement, or the return is inside a branch that didn't execute, or you used braces with an arrow function and forgot return. Check the end of the function — if it falls through without returning, it returns undefined.

What is a callback function?

A function passed to another function to be called later. setTimeout(fn, 1000) takes a callback — fn is called after 1 second. array.map(fn) takes a callback — fn is called for each item. Callbacks are just functions — there's nothing special about the syntax, just how they're used.

What is the difference between function and method?

A function is standalone — call it directly: myFunction(). A method is a function attached to an object — call it through the object: myObject.myMethod(). In JavaScript, methods are just functions stored as properties of objects. The distinction is about how they're called and what this refers to inside them.

How do I pass multiple arguments to a function?

Two ways: as separate parameters — function f(a, b, c) called as f(1, 2, 3), or as an object — function f({ a, b, c }) called as f({ a: 1, b: 2, c: 3 }). The object form is more flexible: order doesn't matter, optional fields work, and adding parameters doesn't break callers. Use it when functions have more than two or three parameters.

🌐 External resources

← Back to JavaScript track
You're in the JavaScript track · Lesson 3