Functions and Scope
Three ways to write a function, how parameters and return values work, what makes a function pure, and a first mental model for closures.
By the end of this lesson, you can
- Write a function as a declaration, an expression, or an arrow function, and explain when the choice matters
- Distinguish a pure function from one with side effects, and explain why pure functions are easier to test
- Trace which variables a function can see based on where it's defined, including a first closure example
- Pass a function as a callback to another function
Why it matters
Functions are how you name a piece of behavior and reuse it. Almost everything past this point in the curriculum — array methods, event handlers, async code — is “a function that takes a function,” so a solid model of what a function can see and what it returns pays off immediately.
Three ways to write a function
// Declaration — hoisted (usable before its own line), has its own `this`
function add(a, b) {
return a + b;
}
// Expression — a function value assigned to a binding, not hoisted the same way
const subtract = function (a, b) {
return a - b;
};
// Arrow function — concise, and does not have its own `this` (see the Intermediate tier for why)
const multiply = (a, b) => a * b;
All three produce a callable function value. A declaration can be called before the line it’s
written on, because the whole function is set up before the script runs; an expression or arrow
function can only be called after its own line has executed, like any other const/let binding.
Parameters, arguments, and return values
A parameter is the name in the function’s definition; an argument is the actual value
passed when calling it. Parameters can have defaults, used when no argument (or undefined) is
passed for that position:
function greet(name, greeting = 'Hello') {
return `${greeting}, ${name}!`;
}
console.log(greet('Ada')); // "Hello, Ada!" — greeting defaults
console.log(greet('Ada', 'Hi')); // "Hi, Ada!"
A function with no return statement returns undefined. This is easy to miss when a function
only prints something and you expect it to hand back a usable value too.
Pure functions
A function is pure if, given the same input, it always returns the same output, and it doesn’t
modify anything outside itself (no reassigning outer variables, no mutating an argument, no
console.log/network call as a side effect that changes behavior).
| Function | Pure? | Why |
|---|---|---|
| (a, b) => a + b | Pure | Same inputs always give the same output; nothing else is touched. |
| () => Math.random() | Impure | Same call gives a different result every time — depends on hidden state. |
| (arr) => arr.push(0) | Impure | Mutates the array passed in — a side effect on the caller's data. |
| (arr) => [...arr, 0] | Pure | Returns a new array; the original argument is untouched. |
Scope, and a first look at closures
A variable is visible within the function (or block) where it’s declared, and any function nested inside that scope — but not the other way around:
const globalValue = 'global';
function outer() {
const outerValue = 'outer';
function inner() {
const innerValue = 'inner';
console.log(globalValue, outerValue, innerValue); // all three visible here
}
inner();
console.log(innerValue); // ReferenceError — innerValue doesn't exist out here
}
A closure is what happens when an inner function keeps access to its outer function’s variables even after the outer function has finished running:
function makeCounter() {
let count = 0;
return function () {
count++;
return count;
};
}
const counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3Callbacks
A callback is simply a function passed as an argument to another function, to be called later:
function processOrder(item, onComplete) {
console.log(`Processing ${item}...`);
onComplete(item);
}
processOrder('book', function (item) {
console.log(`${item} shipped!`);
});
This pattern is everywhere in JavaScript: array methods like map/filter (next lesson),
addEventListener (later this tier), and fetch/promises (later this tier) are all “pass a
function in, it gets called for you.”
Check your understanding
Or reveal the answer without checking
Answer:8
makeAdder(5) returns a function that closes over x = 5. Calling addFive(3) runs that inner function with y = 3, returning 5 + 3 = 8.
Or reveal the answer without checking
Answer:(user) => { user.loginCount++; return user; }
Mutating the passed-in user object (user.loginCount++) is a side effect on the caller's data — the same input object now behaves differently on a second call, which is exactly what purity rules out.
Practice: warm-up
Predict what each call logs, then check:
function describe(name, role = 'engineer') {
console.log(`${name} is an ${role}`);
}
describe('Ada', 'engineer');
describe('Grace');
describe('Katherine', undefined);
Stuck? Reveal one hint at a time.
Hint 1
A default parameter is only used when the argument is missing or explicitly undefined.
Hint 2
describe("Grace") passes no second argument at all — the default applies.
Hint 3
describe("Katherine", undefined) explicitly passes undefined — defaults treat that the same as a missing argument.
Reveal the output
Try the problem yourself before reading this. There is often more than one reasonable approach — treat this as one worked example, not the only correct answer.
Ada is an engineer
Grace is an engineer
Katherine is an engineerPractice: apply it
Write a pure function isValidUsername(name) that returns true only if name is a string between
3 and 20 characters, contains no spaces, and doesn’t start with a digit.
Stuck? Reveal one hint at a time.
Hint 1
typeof name === "string" guards against non-string input before checking anything else.
Hint 2
name.length and name.includes(" ") cover the length and no-spaces rules.
Hint 3
A regular expression isn't necessary yet — checking name[0] against a small set of digit characters, or using Number.isNaN(Number(name[0])), both work; regular expressions get their own lesson soon.
Reveal a reference solution
Try the problem yourself before reading this. There is often more than one reasonable approach — treat this as one worked example, not the only correct answer.
function isValidUsername(name) {
if (typeof name !== 'string') return false;
if (name.length < 3 || name.length > 20) return false;
if (name.includes(' ')) return false;
if (!Number.isNaN(Number(name[0]))) return false; // starts with a digit
return true;
}Summary
- Function declarations are hoisted and callable before their own line; expressions and arrow functions are not.
- A parameter’s default value is used when its argument is missing or explicitly
undefined. - A pure function always returns the same output for the same input and has no side effects — easier to test and reason about, though not every function can or should be pure.
- Scope is nested: an inner function can see its outer function’s variables, but not the reverse.
- A closure is an inner function retaining access to its outer function’s variables even after that outer function has returned — the mechanism behind patterns like a counter that remembers state between calls.
- A callback is just a function passed as an argument to be called later — the pattern behind array methods, event handling, and async code covered in the rest of this tier.