Callbacks and higher-order functions
The last lesson kept passing functions to map without explaining what that
was. A function is a value like any other — it can be stored, passed and
returned. That one fact is behind map, every event listener, and all of
asynchronous JavaScript.
A function is a value
function greet() {
return 'Namaste';
}
console.log(typeof greet);
console.log(typeof greet());
function
string
greet is the function. greet() is the result of running it. One character
of difference, and it is the source of a bug you will write within the week:
button.addEventListener('click', handleClick());
That calls handleClick immediately, at the moment the page loads, and hands its
return value — probably undefined — to addEventListener as the thing to run
on click. So the handler fires once, too early, and then never again.
button.addEventListener('click', handleClick);
Pass the function, do not call it. When you see brackets after a function name in an argument list, stop and check whether you meant them.
Higher-order functions
A function that takes or returns another function. That is the whole definition.
function applyTwice(fn, value) {
return fn(fn(value));
}
const double = (n) => n * 2;
console.log(applyTwice(double, 3));
12
applyTwice knows nothing about doubling. It knows how to apply something
twice, and the caller supplies the something. That is the point: the behaviour
becomes a parameter.
A callback, then
A callback is just a function you hand to something else for it to call later. There is nothing special about it — the name describes the role, not a kind of function.
function forEachOrder(orders, action) {
for (const order of orders) {
action(order);
}
}
forEachOrder(['Priya', 'Arjun'], (name) => {
console.log(`Tiffin for ${name}`);
});
Tiffin for Priya
Tiffin for Arjun
You wrote the loop once and can now do anything per order without touching it.
This is exactly what Array.prototype.forEach is, and module 4 gives you it and
its better-behaved relatives.
Returning a function
The other half of higher-order, and you have already seen it in closures:
function makeRateCalculator(rate) {
return (plates) => plates * rate;
}
const standard = makeRateCalculator(80);
const premium = makeRateCalculator(120);
console.log(standard(3));
console.log(premium(3));
240
360
Two calculators built from one recipe, each closing over its own rate. A
function factory like this is often clearer than a function taking one more
argument, because the rate is decided once, where it is known.
Callbacks that take arguments
The thing calling your callback decides what it passes in. map passes the item,
the index and the whole array:
const items = ['dal', 'rice', 'atta'];
items.forEach((item, index) => {
console.log(`${index + 1}. ${item}`);
});
1. dal
2. rice
3. atta
You may take fewer parameters than you are offered. Extra arguments are
simply ignored, which is why items.forEach((item) => ...) is fine.
That tolerance causes one well-known accident:
console.log(['1', '2', '3'].map(Number));
console.log(['1', '2', '3'].map(parseInt));
[ 1, 2, 3 ]
[ 1, NaN, NaN ]
Number takes one argument, so the extra index is ignored. parseInt takes
two — the string and the base — so it receives the index as the base:
parseInt('2', 1) asks for base 1, which does not exist, giving NaN. Passing
a function by name is convenient and occasionally lethal; when in doubt, wrap it:
.map((s) => parseInt(s, 10)).
Callbacks that report back
Many callbacks are expected to return something. map builds a new array from
whatever you return:
const plates = [3, 5, 2];
const totals = plates.map((count) => count * 80);
console.log(totals);
[ 240, 400, 160 ]
Forget the return and you get an array of undefined:
const broken = plates.map((count) => {
count * 80;
});
console.log(broken);
[ undefined, undefined, undefined ]
The braces made it a block, so the expression is calculated and thrown away. This
is the arrow-function trap from lesson one of this module, in the place you will
actually hit it. Either drop the braces or add the return.
Naming them
Short callbacks read well inline. Longer ones deserve a name:
const isDelivered = (order) => order.status === 'delivered';
const delivered = orders.filter(isDelivered);
orders.filter(isDelivered) reads as a sentence, and isDelivered can be tested
on its own and reused. If a callback is more than about three lines, or you
have written it twice, give it a name.
Where this is going
Every remaining module of this course is built on what you just learnt.
| Where | The callback is |
|---|---|
map, filter, reduce — module 4 |
What to do with each item |
addEventListener — module 7 |
What to do when it is clicked |
setTimeout — module 8 |
What to do after the wait |
.then() — module 8 |
What to do when the data arrives |
The last one is the whole of asynchronous JavaScript. When you write
fetch(url).then(showData), you are passing showData to be called later, by
something that will not be ready for two hundred milliseconds. If passing
functions around feels natural by the end of module 4, module 8 will be much
easier than its reputation.
Check your work
typeof greet is 'function' and typeof greet() is 'string'. The
brackets run it; without them you have the function itself.
addEventListener('click', handleClick()) is the bug. It runs the handler
once at page load and registers its return value — usually undefined — as the
listener. Drop the brackets.
applyTwice(double, 3) is 12. Double 3 to 6, double 6 to 12.
makeRateCalculator(80)(3) is 240. The outer call fixes the rate; the
inner one supplies the plates.
['1','2','3'].map(Number) is [1, 2, 3]; .map(parseInt) is
[1, NaN, NaN]. map passes the index as a second argument, which Number
ignores and parseInt takes as the base. parseInt('2', 1) is NaN because
base 1 does not exist. parseInt('1', 0) happens to work, because base 0 means
"guess".
A map callback with braces and no return gives an array of undefined.
Braces make a block; only an expression body returns implicitly.
Practice
- Print
typeoffor a function and for the result of calling it. Make the difference obvious to yourself. - Write
applyTwice(fn, value)and pass it a doubler, then a function that appends'!'to a string. One function, two behaviours. - Write
forEachOrder(orders, action)and call it twice with different actions — one printing, one counting. - Make the brackets mistake on purpose. Write a function that takes a
callback, pass it
myFn()instead ofmyFn, and work out from the error what was actually handed over. - Write
makeRateCalculator(rate)and build a standard and a premium calculator from it. - Use
forEachwith both(item)and(item, index)and confirm taking fewer parameters is fine. - Run the
parseIntbug. Map['1','2','3']throughNumberand throughparseIntand explain the difference. Then fix it with a wrapper. - Write a
mapcallback with braces and noreturn, get an array ofundefined, then fix it twice — once by removing the braces, once by addingreturn. - Pull a three-line inline callback out into a named function and decide whether the call site reads better.
- Harder. Write
makeLogger(prefix)returning a function that prefixes every message, thenretry(fn, times)that callsfnuntil it returns something truthy or the attempts run out, reporting each attempt through a logger you pass in. You will be passing functions into functions that return functions — which is the point.
That is module three, and it is the one that matters most. You now know that
scope is decided by where code is written, that closures are what happens when a
function outlives that scope, and that this is the single exception — decided
by how a function is called. Those three ideas explain most JavaScript behaviour
that looks like magic.
Next module: objects and arrays — how data is actually shaped, and the array methods you will use every day. All of them take callbacks, so you have just done the hard part.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship