Lesson 0.2 · JavaScript in general
Functions and closures
Arrow functions, parameters, callbacks, functions returning functions, closures and pure functions.
Loading lesson…
Lesson 0.2 · JavaScript in general
Arrow functions, parameters, callbacks, functions returning functions, closures and pure functions.
Loading lesson…
A function is like a kitchen appliance: you put something in (parameters), it does something with it and gives you something back (the return value). A blender gets strawberries and milk and returns a milkshake. You don't need to know how the motor works inside – it's enough to know what goes in and what comes out.
In JavaScript, functions have one more property that is essential for React: a function is a value, just like a number or a piece of text. It can be stored in a variable, passed to another function and even returned from a function. Almost everything you write in React is built on that.
// 1) Function declarationfunction add(a, b) {return a + b}// 2) Function expression – a function stored in a variableconst add = function (a, b) {return a + b}// 3) Arrow function – the most common in modern codeconst add = (a, b) => {return a + b}// 3b) Arrow function with a single expression – no return neededconst add = (a, b) => a + badd(2, 3) // 5 – you call them all the same way
const double = (n) => { return n * 2 } // the full formconst double = (n) => n * 2 // one expression → no { } and no returnconst double = n => n * 2 // one parameter → the parentheses can go// (most code keeps them anyway, for clarity)const greet = () => 'Hello' // no parameters → empty parentheses are requiredconst log = (a, b) => console.log(a, b) // several parameters → parentheses required
| function | arrow function | |
|---|---|---|
| Where you see it | components, named helper functions | callbacks, short functions, event handlers |
| Can it be called before it appears in the code? | yes (hoisting) | no |
this | its own (depends on who calls it) | taken from the surroundings – in modern code you rarely deal with it |
JavaScript doesn't check how many arguments you pass to a function:
function greet(name, greeting) {return `${greeting}, ${name}!`}greet('Anna', 'Hello') // 'Hello, Anna!'greet('Anna') // 'undefined, Anna!' – a missing parameter is undefinedgreet('Anna', 'Hello', 'extra') // 'Hello, Anna!' – an extra one is quietly thrown away
function greet(name, greeting = 'Hello') {return `${greeting}, ${name}!`}greet('Anna') // 'Hello, Anna!'greet('Anna', 'Hi') // 'Hi, Anna!'greet('Anna', undefined) // 'Hello, Anna!' – the default is used for undefined toogreet('Anna', null) // 'null, Anna!' – but not for null!
Three dots before the last parameter collect all the remaining arguments into an array:
function sum(...numbers) {let total = 0for (const n of numbers) { // for…of goes through the array item by itemtotal += n}return total}sum(1, 2) // 3 – numbers is [1, 2]sum(1, 2, 3, 4) // 10 – numbers is [1, 2, 3, 4]sum() // 0 – numbers is []function log(level, ...messages) { // the first argument separately, the rest into an arrayconsole.log(`[${level}]`, messages.join(' '))}log('INFO', 'The', 'user', 'signed', 'in') // [INFO] The user signed in
When a function has many parameters, it's easy to mix up their order. The fix: pass a single object and unpack (destructure) it right in the parameter. This is exactly how props work in React.
// ❌ What do true and false mean? You have to look at the definition.createUser('Anna', 31, true, false)// ✅ Named "parameters" – the order doesn't matterfunction createUser({ name, age, isAdmin = false, newsletter = true }) {return { name, age, isAdmin, newsletter }}createUser({ name: 'Anna', age: 31, isAdmin: true })
function add(a, b) {return a + b // return ends the function and hands back a valueconsole.log('never runs')}function logIt(x) {console.log(x) // without return…}const result = logIt(5) // …the function returns undefined
// ❌ Deep nestingfunction getDiscount(user) {if (user) {if (user.isMember) {if (user.years > 5) {return 20} else {return 10}}}return 0}// ✅ Early return – handle the exceptions first, then the main casefunction getDiscount(user) {if (!user) return 0if (!user.isMember) return 0if (user.years > 5) return 20return 10}
function minMax(numbers) {return { min: Math.min(...numbers), max: Math.max(...numbers) } // an object…}const { min, max } = minMax([3, 9, 1]) // …and unpack itfunction useToggle() {return [value, toggle] // or an array – this is how useState returns values}const [isOpen, toggleOpen] = useToggle()
A function is a value like any other. Tell apart the function itself (greet) and the result of calling it (greet()):
function greet() {return 'Hello'}greet // ƒ greet() – the function itself (the recipe)greet() // 'Hello' – the result of the call (the finished dish)const hello = greet // a second name for THE SAME function, nothing is calledhello() // 'Hello'
When you pass a function as an argument, it's called a callback ("call me back"). The other function calls it when it's needed – in a second, say, or for every item of an array.
function sayHi() {console.log('Hi!')}setTimeout(sayHi, 1000) // ✅ you pass the function – setTimeout calls it in 1 ssetTimeout(sayHi(), 1000) // ❌ you call it RIGHT NOW and pass setTimeout its result (undefined)setTimeout(() => console.log('Hi!'), 1000) // ✅ a callback written right on the spot[1, 2, 3].map((n) => n * 2) // map calls the callback for every item → [2, 4, 6]button.addEventListener('click', () => alert('Click!')) // called on every click
function repeat(times, action) {for (let i = 0; i < times; i++) {action(i) // calls the function it was given}}repeat(3, (i) => console.log('Round', i)) // Round 0, Round 1, Round 2function measure(fn) {const start = performance.now()fn()console.log(`Took ${performance.now() - start} ms`)}measure(() => heavyCalculation())
A function can also make a function and return it. Such a "factory" is called a higher-order function:
function makeGreeter(greeting) {return (name) => `${greeting}, ${name}!`}const english = makeGreeter('Hello') // english is now a FUNCTIONconst spanish = makeGreeter('Hola')english('Anna') // 'Hello, Anna!'spanish('Ana') // 'Hola, Ana!'makeGreeter('Hi')('Peter') // 'Hi, Peter!' – make it and call it straight away// the same, shorterconst makeGreeter = (greeting) => (name) => `${greeting}, ${name}!`
Notice that the english function above knows greeting, even though makeGreeter finished long ago. How is that possible?
When a function is created, it packs a backpack with the variables from the place where it was created. When you later call it anywhere else, it reaches into the backpack. That backpack is called a closure.
function makeCounter() {let count = 0 // this variable goes into the backpackreturn {increment: () => { count++ },get: () => count,}}const a = makeCounter()const b = makeCounter() // a new call = a new backpack with its own counta.increment()a.increment()b.increment()a.get() // 2b.get() // 1 – the counters don't affect each othercount // ❌ ReferenceError – you can't reach count from outside, it's "private"
What you see: A button that calls a plain JS function, makeCounter(). Every call makes a new counter with its own count variable in a closure.
Try it:
count.The takeaway: Every call of a function creates new variables. The functions created during that call carry them along – long after the outer function has finished.
Loading the interactive part…
let message = 'Hello'const show = () => console.log(message)message = 'Hi there'show() // 'Hi there' – the function reads the variable at the moment it's called
In React this has an interesting consequence: every render of a component is a new call of a function with new variables. A function created in one render "sees" the values from exactly that render. This is called a snapshot, and lesson 0.10 and the lesson on state go into it in detail.
A pure function is like a calculator: for the same inputs it always returns the same result and does nothing else. It doesn't change anything outside and doesn't read the clock or chance.
// ✅ Pureconst add = (a, b) => a + bconst fullName = (user) => `${user.first} ${user.last}`const withVat = (price) => Math.round(price * 1.21)// ❌ Impure – the result depends on something other than the inputconst now = () => new Date() // a different time every timeconst roll = () => Math.random() // a different number every timeconst total = (price) => price + tax // reads an outer variable that can change// ❌ Impure – they have a side effect (they change the world around them)let count = 0const increment = () => { count++ } // changes an outer variableconst addItem = (list, x) => { list.push(x) } // changes the array it was givenconst save = (data) => localStorage.setItem('data', data)const log = (x) => console.log(x)
Fill in the bodies of two arrow functions. Each one fits on a single line.
CHALLENGE: Two arrow functions 1. square(3) should return 9 (the number times itself). 2. isEven(4) should return true, isEven(5) false. An even number leaves a remainder of 0 when divided by two: n % 2 === 0
Loading the interactive part…
Six small functions that make other functions or receive them as arguments. You'll practise parameters, callbacks and closures.
Medium and hard challenges are a bonus for signed-in readers. Signing up is free – just an e-mail, no password.
return. Return an object in ({ … }).undefined, default values with =, any number with ...rest. Lots of parameters → a single object.fn is the function, fn() is its result. Pass a callback without parentheses, or wrap it in () => fn(arg).Checks, Check)Checks, Check – from the file src/course/checks.tsx. The course helper for function-style challenges: runs the tests and shows ✅/❌. You don't need to know how it works inside – it just tells you which steps are done./** One test: a description and a function returning true (passed) or false. It may be async. */export type Check = [name: string, test: () => boolean | Promise<boolean>]/*** Runs the tests of a challenge and shows ✅ or ❌ for each.* An error thrown in a test (e.g. when trying to change a frozen object) is shown under the test.*/export function Checks({ tests }: { tests: Check[] }) {const t = useT()const [results, setResults] = useState<Result[]>(() => tests.map(() => ({ status: 'pending' })))useEffect(() => {let ignore = falsetests.forEach(([, test], i) => {Promise.resolve().then(test).then((ok): Result => (ok ? { status: 'ok' } : { status: 'fail' }),(e): Result => ({ status: 'fail', error: e instanceof Error ? e.message : String(e) }),).then((result) => {if (ignore) returnsetResults((prev) => prev.map((r, idx) => (idx === i ? result : r)))})})return () => {ignore = true}}, [tests])const passed = results.filter((r) => r.status === 'ok').lengthreturn (<div className="stack"><strong className={passed === tests.length ? 'ok' : undefined}>{t.checksPassed(passed, tests.length)}</strong><ul className="list">{tests.map(([name], i) => {const r = results[i] ?? { status: 'pending' }return (<li key={name}>{r.status === 'pending' ? '⏳' : r.status === 'ok' ? '✅' : '❌'} {name}{r.status === 'fail' && r.error && (<div className="bad muted">{t.checksError} {r.error}</div>)}</li>)})}</ul></div>)}