Lesson 2 of 1011 min
Functions and scope
Functions package behaviour you can reuse. Scope decides which names a function can see, and closures let it remember them.
In this lesson
- Write function declarations and arrow functions
- Use default and rest parameters
- Explain block scope and closures
Three ways to write a function#
A function takes inputs (parameters), does some work and usually returns a value.
function add(a: number, b: number): number {1 return a + b;}const multiply = function (a: number, b: number): number {2 return a * b;};const subtract = (a: number, b: number): number => a - b;3console.log(add(2, 3), multiply(2, 3), subtract(2, 3)); // 5 6 -1A declaration. It is hoisted: you can call it above the line where it is written.
A function expression stored in a constant.
An arrow function. With a single expression, the value is returned without writing return.
The : number annotations are TypeScript. Plain JavaScript is the same without them; we keep them because they make the examples easier to read and you will meet TypeScript properly in a later module.
Default and rest parameters#
function greet(name: string, greeting = "Hello"): string {1 return `${greeting}, ${name}!`;}function sum(...numbers: number[]): number {2 let total = 0; for (const n of numbers) total += n; return total;}console.log(greet("Budi")); // "Hello, Budi!"console.log(greet("Budi", "Hi")); // "Hi, Budi!"console.log(sum(1, 2, 3, 4)); // 10A default value is used when the argument is missing (or undefined).
...numbers collects any number of arguments into an array.
Scope: where a name is visible#
Names created with const and let exist only inside the nearest pair of curly braces, the block.
const message = "outside";if (message.length > 0) { const inner = "inside"; console.log(message, inner); // both visible here}console.log(inner); // error: inner does not exist out hereInner blocks can see outer names, never the other way round. This keeps names from leaking and colliding.
Closures: functions that remember#
A function keeps access to the variables that were in scope where it was created, even after the outer function has finished. That bundle is a closure.
function createCounter() { let count = 0;1 return function increment(): number {2 count += 1; return count; };}const next = createCounter();console.log(next(), next(), next()); // 1 2 3count lives in createCounter's scope. Nothing outside can touch it directly.
increment still sees count after createCounter returned, so each call updates the same variable.
Closures are everywhere: event handlers, timers, React hooks and the debounce helpers you will write later all rely on them.
Arrow function or declaration?
Declarations read well for named, top-level helpers and are hoisted. Arrow functions are shorter for callbacks (items.map((x) => x * 2)) and do not create their own this. Many teams use declarations for top-level functions and arrows for everything passed as an argument.
ExerciseTwo independent counters
Call createCounter() twice to make a and b. Call a() twice and b() once. What does each return, and why are they independent?
Show solution
a() returns 1 then 2; b() returns 1. Every call to createCounter runs the function again and creates a new count variable, so each returned function closes over its own copy.
Recap#
- Declarations, expressions and arrow functions all create functions; arrows are compact for callbacks.
- Default parameters fill gaps; rest parameters collect extras.
const/letare block-scoped, and closures let functions remember their surroundings.