1. Three Ways to Write a Function
JavaScript has three distinct syntaxes for creating a function — they overlap a lot, but each has a situation where it's the right (or only) choice:
function greet(name) {
return `Hello, ${name}!`;
}
greet("Ada"); // "Hello, Ada!"
const greet = function (name) {
return `Hello, ${name}!`;
};
greet("Ada"); // "Hello, Ada!"
const greet = (name) => {
return `Hello, ${name}!`;
};
// A single expression can skip the braces and `return` entirely:
const greetShort = (name) => `Hello, ${name}!`;
greetShort("Ada"); // "Hello, Ada!"
A function declaration is hoisted in full (more on that below) and
reads clearly as a named, top-level operation. A function expression
stores an (often anonymous) function in a variable, useful when you want to pass a
function around like any other value. An arrow function is the
terse, modern default for short callbacks and one-line logic — and, importantly,
doesn't have its own this (that distinction matters starting Week 3).
Use a named function declaration for top-level, reusable logic — it reads clearly and is hoisted, so declaration order doesn't matter. Use an arrow function for short callbacks passed directly into something else, like array.map(x => x * 2). You'll see both constantly in real code.
2. Default & Rest Parameters
Two features make function signatures far more flexible than a fixed parameter list:
function greet(name, greeting = "Hello") {
return `${greeting}, ${name}!`;
}
greet("Ada"); // "Hello, Ada!"
greet("Ada", "Hi"); // "Hi, Ada!"
function sum(...numbers) {
return numbers.reduce((total, n) => total + n, 0);
}
sum(1, 2, 3); // 6
sum(10, 20, 30, 40); // 100
A default parameter only kicks in when the argument is undefined —
passing null or 0 explicitly does not trigger the
default. The rest parameter (...numbers) must come last in the
parameter list, and collects any number of remaining arguments into a real array —
useful whenever a function needs to accept a variable number of inputs.
3. Block vs. Function Scope
Scope determines where a variable is visible from. This is exactly
where let/const and var genuinely diverge:
if (true) {
let blockScoped = "only visible in here";
console.log(blockScoped); // works
}
console.log(blockScoped); // ReferenceError: blockScoped is not defined
if (true) {
var functionScoped = "leaks out of the block";
}
console.log(functionScoped); // "leaks out of the block" -- no error!
let and const are scoped to the nearest enclosing
{'{'}...{'}'} block — an if, a for loop, any
block at all. var ignores block boundaries entirely and is scoped to
the nearest enclosing function (or the global scope, if there is no
enclosing function) — which is exactly why it leaked out of the if
block above. This is the single biggest practical reason to avoid var:
its scope doesn't match what the code's indentation visually suggests.
4. Hoisting
Hoisting is JavaScript's behavior of processing certain declarations before running the rest of the code in a scope — but the three declaration types behave very differently under it:
sayHi(); // "Hi!" -- works, even though called before the declaration below
function sayHi() {
console.log("Hi!");
}
console.log(x); // ReferenceError: Cannot access 'x' before initialization
let x = 5;
console.log(y); // undefined -- no error, but not the value either
var y = 5;
A function declaration is hoisted with its entire body, so it can be
called before the line it's written on. let and const are
technically hoisted too, but stay in an inaccessible "temporal dead zone" until
their declaration line actually runs — accessing them earlier throws, rather than
silently returning undefined. var is hoisted and
pre-initialized to undefined, which is exactly the kind of
silent-bug-waiting-to-happen behavior that makes let/const
the safer default.
Only a function name() {'{}'} declaration gets the full hoisting treatment. const greet = function () {'{}'} or const greet = () => {'{}'} follow const's hoisting rules instead — calling greet() before that line throws, exactly like any other const.
5. Return Values & Pure Functions
A function that doesn't explicitly return anything returns
undefined. Beyond that, a useful habit to build from week one: prefer
writing pure functions — functions whose output depends only on
their inputs, with no side effects on anything outside them:
let total = 0;
// Impure -- depends on and mutates something outside itself
function addToTotal(n) {
total += n;
}
// Pure -- same inputs always produce the same output, nothing external touched
function add(a, b) {
return a + b;
}
add(2, 3); // always 5, no matter what else is happening in the program
Pure functions are dramatically easier to test, reason about, and reuse — a
function like add can be called anywhere, any number of times, and
never surprises you. This habit pays off even more once state management shows up
in a framework course later.
6. Hands-on Exercise
Build a small shopping-cart total calculator
Apply function declarations, default/rest parameters and pure-function thinking together.
Requirements:
- Create a file
cart.js. Write a pure functionfunction priceWithTax(price, taxRate = 0.08)that returns the price including tax. - Write a second pure function
function cartTotal(...prices)using a rest parameter, that sums an array of prices usingreduce. - Rewrite
cartTotalas an arrow function assigned to aconst, and confirm it still works identically. - Call
cartTotalwith at least four prices, log the raw total, then log the total after passing it throughpriceWithTax. - Add a comment above each function explaining, in one sentence, why it qualifies as "pure."
If reduce is unfamiliar, it's covered in full in Week 5 — for now, numbers.reduce((total, n) => total + n, 0) is a safe pattern to copy directly to sum an array.
7. Knowledge Check
Four quick questions. Expand each to check your answer.
Q1
Why can a function declaration be called before the line it's written on, but a function expression can't?
Why can a function declaration be called before the line it's written on, but a function expression can't?
A function name() {'{}'} declaration is fully hoisted — its entire definition is processed before the rest of the code in that scope runs. A function expression like const greet = function () {'{}'} is just a const assignment, so it follows const's own hoisting rules: the variable exists but is inaccessible (the "temporal dead zone") until its declaration line actually executes.
Q2
A variable declared with var inside an if block is still accessible after the block ends. Why?
A variable declared with var inside an if block is still accessible after the block ends. Why?
var is function-scoped, not block-scoped — it ignores {'{'}...{'}'} boundaries entirely and belongs to the nearest enclosing function (or the global scope). let and const, by contrast, are genuinely block-scoped and would correctly throw a ReferenceError if accessed outside the if block.
Q3
Given function greet(name, greeting = "Hello"), what does greet("Ada", null) return?
Given function greet(name, greeting = "Hello"), what does greet("Ada", null) return?
"null, Ada!" (i.e. greeting is literally null) — a default parameter only activates when the argument is undefined. Passing null explicitly is a real, intentional value as far as the default parameter is concerned, so the default is skipped. Only calling greet("Ada") — omitting the second argument entirely — would trigger "Hello".
Q4
Why are pure functions easier to test than functions with side effects?
Why are pure functions easier to test than functions with side effects?
A pure function's output depends only on its own inputs and touches nothing outside itself, so calling it with the same arguments always produces the same result — a test just checks input against expected output, with no need to set up or inspect any external state. A function with side effects (like mutating an outer variable) requires a test to also track and verify that external state, which is more work and more fragile.