JavaScript 02 · JavaScript in general
Functions and closures
Arrow functions, parameters, callbacks, functions returning functions, closures and pure functions.
Loading lesson…
JavaScript 02 · 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!'
You don't have to store the new function in a variable – you can call it straight away. Two pairs of parentheses in a row look odd, but you simply read them left to right: the first call runs makeGreeter, which returns a function, and the second call runs that returned function.
makeGreeter('Hi')('Peter') // 'Hi, Peter!'// the same in two steps:const greet = makeGreeter('Hi') // 1. make the functiongreet('Peter') // 2. call it → 'Hi, Peter!'
The factory can also be written shorter, as two arrow functions in a row. The first arrow takes greeting and returns the second arrow, which takes name:
const makeGreeter = (greeting) => (name) => `${greeting}, ${name}!`
Notice that the english function above knows greeting, even though makeGreeter finished long ago. How is that possible?
Normally the variables created inside a function (its parameters and everything you declare in it with let and const) live only while that call runs. The function finishes and the variables are gone, like a whiteboard wiped clean after class.
But a function created inside another function packs a backpack when it's created, with all the variables it can see at that spot. As long as this inner function exists, so does the backpack, so the variables in it don't disappear. When you later call it anywhere else, it reaches into the backpack. A function together with its backpack is called a closure.
function makeGreeter(greeting) {// greeting is a variable of makeGreeterreturn (name) => `${greeting}, ${name}!`// ↑ this arrow is created here, inside makeGreeter, and can see greeting,// so it packs it into its backpack}const english = makeGreeter('Hello')// makeGreeter has finished. Without a closure greeting would be gone, but the arrow// (now stored in english) has it in its backpack, so greeting lives on.english('Anna') // the arrow reaches into the backpack: greeting = 'Hello' → 'Hello, Anna!'
Step by step:
makeGreeter('Hello') is called and a variable greeting with the value 'Hello' is created.(name) => … is created inside. It packs the variables it can see at that spot into its backpack, greeting included.makeGreeter returns the arrow and finishes. The arrow is stored in english and the backpack goes with it.english('Anna') calls the arrow. It gets name from the argument and takes greeting out of the backpack.A function can not only read from its backpack but also change the variables in it. And when one call creates several functions, they share one common backpack. That's what this counter factory is built on:
function makeCounter() {let count = 0 // a variable of this particular makeCounter callreturn {increment: () => { count++ }, // both arrows were created in the same call,get: () => count, // so they share one backpack with one count}}const a = makeCounter() // 1st call → backpack A with its own count = 0const b = makeCounter() // 2nd call → backpack B with another count = 0a.increment() // count in backpack A: 1a.increment() // count in backpack A: 2b.increment() // count in backpack B: 1a.get() // 2 – get reads the same count that increment changedb.get() // 1 – the counters don't affect each othercount // ❌ ReferenceError – you can't reach count from outside, it's "private"
Notice three things. increment and get work with the same variable, so get sees what increment changed. Every call of makeCounter() creates a new count, so a and b don't affect each other. And because count lives only in the backpack, nobody can overwrite it from outside: the only way to change it is increment.
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…
The backpack doesn't make a copy of the values when the function is created. It carries the variables themselves and reads their values only when you call the function. If a variable changes in the meantime, the function sees the new value:
let message = 'Hello'const show = () => console.log(message) // show has the variable message in its backpackmessage = 'Hi there' // change the variable (nobody has called show yet)show() // 'Hi there' – show reads message only now, at the moment it's called
So the function "looks at" the variable rather than keeping a photo of it. That's why the counter above works: get reads the current count every time.
Every function you write inside a component (a click handler, a callback for map, a function in an effect) is a closure. A component is a plain function, so everything in it is in the backpack: props, state and any other variables.
function Greeting({ name }) {// handleClick is created inside the component → name is in its backpackconst handleClick = () => alert(`Hello, ${name}!`)return <button onClick={handleClick}>Say hello</button>}
There's a catch, though. Every render of a component is a new call of a function, so it gets a new backpack with new variables. A handler created in one render has the variables from exactly that render in its backpack, and later renders don't change them (state in them is a const). This is called a snapshot, and lesson JavaScript 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)
The skeletons of two arrow functions return a fixed value for now. Rewrite what stands after return so they work with the number they get.
Task is in the comments in the code below – at the top of the file and at the places marked TODO.
Loading the interactive part…
Six small functions. Three of them (makeGreeter, makeMultiplier, once) don't return a value – they create and return a new function; the others receive a function as an argument or return an object with functions. You'll practise parameters, callbacks and closures. What exactly each function should do is in the comments in the code.
Medium and hard challenges unlock once you sign in. Like the whole course, they are free – just an e-mail, no password and no payment.
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).