Write a few loops and the same seven lines start appearing in three places with different names in them, which is the point at which copying stops being cheap.
A function names a computation. You write it once, you call it wherever you want the result, and a fix reaches every caller. That much is obvious. What is not obvious, and is the real subject of this lesson, is the contract a function makes: what it takes, what it gives back, and what it touches on the way. If you have arrived here directly, all that is assumed is const, let, if and a loop.
Naming a computation
The declaration form is the one to learn first:
function celsiusToFahrenheit(c) {
return c * 9 / 5 + 32;
}
console.log(celsiusToFahrenheit(37));
// 98.6function opens the declaration, celsiusToFahrenheit is the name, c in brackets is the parameter, and the block is the body. Calling it is name(value). The value you pass, 37, is the argument. Parameter and argument are two ends of the same act, and the distinction is worth keeping: the parameter is the name inside the function, the argument is the value supplied at the call. Confusing them is what produces sentences like "the argument is undefined", which is usually not what the speaker means.
A function is defined once and called any number of times, and each call is independent. celsiusToFahrenheit(20) gives 68 and celsiusToFahrenheit(-40) gives -40, which is the one temperature where the two scales agree and a pleasant thing to check.
Names matter more here than anywhere else in a program, because the name is the only thing most readers will ever see. A function called calc tells you nothing; one called celsiusToFahrenheit tells you the whole contract. The working rule is that a function that answers a question gets a noun phrase for a name, totalWithTax, and one that does something gets a verb phrase, sendReceipt. If you cannot name it, you have not decided what it does, and writing the body will not help.
return gives a value back
return ends the function immediately and hands a value to whoever called it. Everything after it in that function is not executed, which is exactly what makes the guard clauses of the decisions lesson work.
The commonest confusion in a first course is between returning and printing, and it is worth being blunt about it. console.log puts text on a screen for a human. return produces a value for the rest of the program. They are not alternatives:
function addBad(a, b) {
console.log(a + b);
}
function addGood(a, b) {
return a + b;
}
const x = addBad(2, 3); // prints 5, and x is undefined
const y = addGood(2, 3); // prints nothing, and y is 5
console.log(y * 10); // 50addBad cannot be used in a larger expression, because the value went to the screen and nothing came back. A function with no return returns undefined, and so does one that falls off the end of its body, which is the source of a whole family of confusing failures: you call a function, get undefined, and the actual bug is a missing return several lines up. If a function is meant to answer a question, every path out of it needs a return.
There is a small syntactic trap in the same area. A return with its value on the next line is broken:
return
a + b; // returns undefined, alwaysJavaScript inserts a semicolon after a bare return, so the expression below is unreachable. Keep the value on the same line as the return.
Example. What does each of these print?
function area(w, h) {
w * h;
}
console.log(area(3, 4));It prints undefined. The body computes 12 and then throws it away: the line is an expression statement with no return in front of it, so the function ends without giving anything back. Adding return before w * h makes it print 12. Note that nothing warns you, because computing a value and discarding it is legal and occasionally intended.
Now you. What does this print, and why?
function bigger(a, b) {
if (a > b) {
return a;
}
}
console.log(bigger(3, 9));Answer
It prints undefined. The condition 3 > 9 is false, so the return never runs, and the function falls off the end of its body. Only one of the two paths returns a value. Adding return b; after the if fixes it, and the guard clause style would have written it as two returns from the start.
Parameters, arguments, and what happens when they disagree
JavaScript does not check the number of arguments. Call a two parameter function with one argument and the missing parameter is undefined; call it with three and the extra is discarded. Neither is an error, which is a genuine weakness of the language and a reason to be careful:
function power(base, exponent = 2) {
return base ** exponent;
}
console.log(power(5)); // 25
console.log(power(5, 3)); // 125A default value makes the omission deliberate rather than accidental. The default is used when the argument is undefined, including when it is left off entirely, and it is not used for null: power(5, null) gives 1, because null coerces to 0 in the arithmetic. That asymmetry is the same distinction between "nothing was supplied" and "nothing is meant to be here" from the first lesson.
Arguments are passed by value. The function receives a copy of the value, so rebinding a parameter inside the function has no effect on the caller's name:
function bump(n) {
n = n + 1;
return n;
}
let count = 5;
console.log(bump(count)); // 6
console.log(count); // 5That is the whole story for numbers, strings and booleans. It is not the whole story for arrays and objects, where the value being copied is a reference, and two names end up reaching one thing. That is a large enough trap to have its own lesson later in this course, and it is flagged here so that the rule you carry forward is "the value is copied" rather than the vaguer "the function gets its own copy of everything".
Scope: what a function can see
A name declared with let or const inside a block exists only inside that block. A function body is a block, so its parameters and its local names are invisible from outside:
function f() {
const secret = 42;
return secret;
}
console.log(secret); // ReferenceError: secret is not definedLooking a name up works outward. Inside a function, the machine looks in the function's own body, then in the block enclosing it, then outward again until it reaches the top level of the file. So a function can read a name declared outside it, and that is how it reaches other functions and shared constants.
When an inner name has the same spelling as an outer one, the inner one shadows it, and the outer one is untouched:
let n = 1;
function g() {
let n = 2;
return n;
}
console.log(g(), n);
// 2 1Shadowing is not a mistake in itself and is often exactly what you want, since it means a parameter called total cannot collide with somebody else's total. It becomes a mistake when it is accidental, and it is one of the reasons to keep the number of names visible at the top level of a file small.
What else a function touches
The contract has a third clause that beginners rarely think about and everybody who has maintained a program thinks about constantly. Besides taking arguments and returning a value, a function may do something to the world: print, write a file, send a request, or change a name declared outside it. Anything of that kind is a side effect.
A function with no side effects, whose result depends only on its arguments, is called pure. celsiusToFahrenheit is pure. So is power. Pure functions have three properties worth having. They are testable, because you can call one with known inputs and compare the result to a known answer with no setup at all. They are safe to move, since calling one in a different order or a different place cannot break anything else. And they can be understood alone: everything that decides the answer is visible in the call.
Impure functions are unavoidable, since a program with no effects on the world does nothing observable. The design that survives contact with a large program is to keep them separated: put the calculation in pure functions and let a thin layer of impure code fetch the input, call them and display the output. When people say a program is testable, this is very often all they mean.
Two smaller things make a function impure in a way that surprises people. Math.random() returns a different value each call, so any function using it is impure by construction and cannot be tested by comparing against a fixed answer; the usual fix is to pass the random value in as an argument. Date.now() is the same, and a function that reads the clock will pass its tests today and fail them in a leap year.
Example. Which of these is pure, and what makes the difference?
let rate = 0.2;
function taxA(amount) {
return amount * rate;
}
function taxB(amount, rate) {
return amount * rate;
}taxB is pure: everything it uses arrives as an argument, so taxB(80, 0.2) is 16 today and 16 forever. taxA reads a name from outside itself, so its answer depends on a value the caller cannot see and someone else can change. It has no side effect, but it is not pure, and it is not testable without setting up the surrounding state first. The distinction matters most when the outside name is changed by another function halfway through a run.
Now you. Say whether each is pure, and why.
function roll() {
return Math.floor(Math.random() * 6) + 1;
}
function total(price, quantity) {
console.log("computing");
return price * quantity;
}Answer
Neither. roll returns a different value for the same (empty) arguments, so it depends on something other than its inputs. total computes its answer purely but also writes to the console, which is an effect on the world; removing the console.log would make it pure. Note that the two are impure for different reasons, one in the input and one in the output, and only the second is easy to remove.
Designing the contract before the body
Writing a function well is mostly deciding three things before typing the body, and it takes about a minute.
What comes in, with the types and any restriction on them. bmi(kg, metres) takes two positive numbers. That restriction is a precondition, and you have to choose what happens when it is broken. The three honest options are to return a sentinel such as null, to throw an error, or to state in a comment that behaviour is undefined for bad input and check at the call site. What is not honest is quietly returning a number computed from nonsense, since that is how a negative weight becomes a plausible looking figure in a report.
What goes out, one thing, of one type. A function that returns a number sometimes and a string apology other times forces every caller to check which, and the checking spreads. Where two kinds of outcome genuinely exist, later lessons return an object with both, or throw.
What it touches. Ideally nothing. If it must, say so in the name: saveUser obviously writes, getUser obviously does not, and a getUser that quietly writes a cache entry is the sort of thing that costs somebody a day.
Example. Write bmi(kg, metres) returning body mass index to one decimal place, with null for non-positive input, and check it for 70 kg at 1.75 m.
function bmi(kg, metres) {
if (kg <= 0 || metres <= 0) return null;
return Math.round((kg / (metres * metres)) * 10) / 10;
}
console.log(bmi(70, 1.75));
// 22.9The guard states the precondition and returns a value the caller can test for. The arithmetic is , which rounds to 22.9. Rounding by multiplying by 10, rounding, and dividing by 10 is the standard idiom, and the previous lesson's warning applies: the result is a floating point number and the last digit is only as exact as the format allows.
Now you. Write discountedPrice(price, percentOff) that returns the price after the discount, refuses a percentage outside 0 to 100 by returning null, and leaves the price untouched at 0 per cent. What does it give for 80 and 25?
Answer
function discountedPrice(price, percentOff) {
if (percentOff < 0 || percentOff > 100) return null;
return price * (1 - percentOff / 100);
}For 80 and 25 it gives . At 0 per cent the factor is 1, so the price comes back unchanged, and at 100 per cent it is 0, both of which are worth checking because they are the boundaries. The function is pure, so those three checks are the whole test.
Functions are values too
One more fact, stated now because the next lessons lean on it. A function is itself a value: it can be bound to a name, passed as an argument, and returned from another function. Beside the declaration form there is the arrow form, which is shorter and appears constantly in real code:
const double = x => x * 2;
const add = (a, b) => a + b;With one parameter the brackets are optional, and with a single expression as the body the braces and the return are both implied. There are differences between the two forms beyond looks, involving this, that do not matter until you write classes, and the practical advice is to use declarations for named top level functions and arrows for the small ones you pass to something else. Passing a function to another function is what the map, filter and reduce lesson is built on.
One value per name is not enough
Functions have fixed the duplication problem, and a program can now be written as a set of named computations that call each other. The limitation is in what can be passed between them.
Every name so far holds exactly one value. A function to average three numbers can take three parameters; a function to average a thousand cannot, and a function to average an unknown number of them is not expressible at all. The same wall appears anywhere data arrives in quantity, which is almost everywhere. What is needed is a single value that holds many values, in order, with a way to reach each one. That is an array, and it is next.