BeginnerFunctions and scope~35 min

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).

Pure versus impure — same-looking calls, different guarantees
FunctionPure?Why
(a, b) => a + bPureSame inputs always give the same output; nothing else is touched.
() => Math.random()ImpureSame call gives a different result every time — depends on hidden state.
(arr) => arr.push(0)ImpureMutates the array passed in — a side effect on the caller's data.
(arr) => [...arr, 0]PureReturns 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:

JavaScriptA closure remembering count between calls
function makeCounter() {
  let count = 0;
  return function () {
    count++;
    return count;
  };
}

const counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3

Callbacks

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

What does this log? function makeAdder(x) { return function (y) { return x + y; }; } const addFive = makeAdder(5); console.log(addFive(3));
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.

Which of these functions is impure?
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.

  1. Hint 1

    A default parameter is only used when the argument is missing or explicitly undefined.

  2. Hint 2

    describe("Grace") passes no second argument at all — the default applies.

  3. 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 engineer

Practice: 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.

  1. Hint 1

    typeof name === "string" guards against non-string input before checking anything else.

  2. Hint 2

    name.length and name.includes(" ") cover the length and no-spaces rules.

  3. 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.