JavaScript Functions
Posted on May 27, 2019 in JavaScript by Matt Jennings
Note: Information below was taken from Eloquent JavaScript, 3rd edition, Chapter 3 Functions.
Function Definition
A function definition is a regular binding where the value of the binding is a function. For example, this code defines square
to refer to a function that produces the square of a given number:
const square = function(x) { return x * x; }; console.log(square(12)); // 144
A function is created with an expression that starts with the keyword function
. Functions have a set of parameters (in this case, only x
) and a body, which contains the statements that are to be executed when the function is called.
A return
statement determines the value the function returns. When control comes across such a statement, it immediately jumps out of the current function and gives the returned value to the code that called the function. A return
keyword without an expression after it will cause the function to return undefined
. Functions that don’t have a return
statement at all, such as makeNoise
, similarly return undefined
.
Parameters to a function behave like regular bindings, but their initial values are given by the caller of the function, not the code in the function itself.
Bindings and scopes
Each binding has a scope, which is the part of the program in which the binding is visible. For bindings defined outside of any function or block, the scope is the whole program—you can refer to such bindings wherever you want. These are called global.
But bindings created for function parameters or declared inside a function can be referenced only in that function, so they are known as local bindings. Every time the function is called, new instances of these bindings are created.
Bindings declared with let
and const
are in fact local to the block that they are declared in, so if you create one of those inside of a loop, the code before and after the loop cannot “see” it.
In pre-2015 JavaScript, only functions created new scopes, so old-style bindings, created with the var
keyword, are visible throughout the whole function that they appear in—or throughout the global scope, if they are not in a function.
Nested scope
JavaScript distinguishes not just global and local bindings. Blocks and functions can be created inside other blocks and functions, producing multiple degrees of locality.
For example, this function—which outputs the ingredients needed to make a batch of hummus—has another function inside it:
const hummus = function(factor) { const ingredient = function(amount, unit, name) { let ingredientAmount = amount * factor; if (ingredientAmount > 1) { unit += "s"; } console.log(`${ingredientAmount} ${unit} ${name}`); }; ingredient(1, "can", "chickpeas"); ingredient(0.25, "cup", "tahini"); ingredient(0.25, "cup", "lemon juice"); ingredient(1, "clove", "garlic"); ingredient(2, "tablespoon", "olive oil"); ingredient(0.5, "teaspoon", "cumin"); }; hummus(2) /* "2 cans chickpeas" "0.5 cup tahini" "0.5 cup lemon juice" "2 cloves garlic" "4 tablespoons olive oil" "1 teaspoon cumin" */
Functions as values
A function value can do all the things that other values can do—you can use it in arbitrary expressions, not just call it. It is possible to store a function value in a new binding, pass it as an argument to a function, and so on. Similarly, a binding that holds a function is still just a regular binding and can, if not constant, be assigned a new value, like so:
let launchMissiles = function() { missileSystem.launch("now"); }; if (safeMode) { launchMissiles = function() {/* do nothing */}; }
Function Declaration
function square(x) { return x * x; }
This is a function declaration. The statement defines the binding square
and points it at the given function. It is slightly easier to write and doesn’t require a semicolon after the function.
There is one subtlety with this form of function definition.
console.log("The future says:", future()); function future() { return "You'll never have flying cars"; }
The preceding code works, even though the function is defined below the code that uses it. Function declarations are not part of the regular top-to-bottom flow of control. They are conceptually moved to the top of their scope and can be used by all the code in that scope. This is sometimes useful because it offers the freedom to order code in a way that seems meaningful, without worrying about having to define all functions before they are used.
Arrow functions
There’s a third notation for functions, which looks very different from the others. Instead of the function
keyword, it uses an arrow (=>
) made up of an equal sign and a greater-than character (not to be confused with the greater-than-or-equal operator, which is written >=
).
const power = (base, exponent) => { let result = 1; for (let count = 0; count < exponent; count++) { result *= base; } return result; }; console.log(power(2, 10)) // 1024
When there is only one parameter name, you can omit the parentheses around the parameter list. If the body is a single expression, rather than a block in braces, that expression will be returned from the function. So, these two definitions of square
do the same thing:
const square1 = (x) => { return x * x; }; const square2 = x => x * x;
The call stack
The way control flows through functions is somewhat involved. Let’s take a closer look at it. Here is a simple program that makes a few function calls:
function greet(who) { console.log("Hello " + who); } greet("Harry"); console.log("Bye"); // "Hello Harry" // "Bye"
Because a function has to jump back to the place that called it when it returns, the computer must remember the context from which the call happened. In one case, console.log
has to return to the greet
function when it is done. In the other case, it returns to the end of the program.
The place where the computer stores this context is the call stack. Every time a function is called, the current context is stored on top of this stack. When a function returns, it removes the top context from the stack and uses that context to continue execution.
Storing this stack requires space in the computer’s memory. When the stack grows too big, the computer will fail with a message like “out of stack space” or “too much recursion”. The following code illustrates this by asking the computer a really hard question that causes an infinite back-and-forth between two functions. Rather, it would be infinite, if the computer had an infinite stack. As it is, we will run out of space, or “blow the stack”.
function chicken() { return egg(); } function egg() { return chicken(); } console.log(chicken() + " came first."); // "RangeError: Maximum call stack size exceeded"
Optional Arguments
The following code is allowed and executes without any problem:
function square(x) { return x * x; } console.log(square(4, true, "hedgehog")); // 16
We defined square
with only one parameter. Yet when we call it with three, the language doesn’t complain. It ignores the extra arguments and computes the square of the first one.
Closure
function wrapValue(n) { let local = n; return () => local; } let wrap1 = wrapValue(1); let wrap2 = wrapValue(2); console.log(wrap1()); // → 1 console.log(wrap2()); // → 2
This is allowed and works as you’d hope—both instances of the binding can still be accessed. This situation is a good demonstration of the fact that local bindings are created anew for every call, and different calls can’t trample on one another’s local bindings.
This feature—being able to reference a specific instance of a local binding in an enclosing scope—is called closure. A function that references bindings from local scopes around it is called a closure. This behavior not only frees you from having to worry about lifetimes of bindings but also makes it possible to use function values in some creative ways.
Recursion
It is perfectly okay for a function to call itself, as long as it doesn’t do it so often that it overflows the stack. A function that calls itself is called recursive. Recursion allows some functions to be written in a different style. Take, for example, this alternative implementation of power
:
function power(base, exponent) { if (exponent == 0) { return 1; } else { return base * power(base, exponent - 1); } } console.log(power(2, 3)); // 8
Functions and side effects
A pure function is a specific kind of value-producing function that not only has no side effects but also doesn’t rely on side effects from other code—for example, it doesn’t read global bindings whose value might change.
A pure function has the pleasant property that, when called with the same arguments, it always produces the same value (and doesn’t do anything else). A call to such a function can be substituted by its return value without changing the meaning of the code. When you are not sure that a pure function is working correctly, you can test it by simply calling it and know that if it works in that context, it will work in any context. Nonpure functions tend to require more scaffolding to test.
Recursion
function isEven(n) { n = Math.abs(n); if (n === 0) return true; else if (n === 1) return false; else return isEven(n - 2); } console.log(isEven(-11)) // false
Bean counting
function countBs(string) { let arr1 = [...string] let arr2 = arr1.filter((elem) => { if(elem === 'B') { return elem } }) return arr2.length } console.log(countBs('BBC')) // 2
Next, write a function called countChar
that behaves like countBs
, except it takes a second argument that indicates the character that is to be counted (rather than counting only uppercase “B” characters). Rewrite countBs
to make use of this new function.
function countChar(string, character) { const arr1 = string.split('') const arr2 = arr1.filter((elem) => { if(elem === character) { return character } }) return arr2.length } console.log(countChar('kksdafaosdfjsfiewkkk', 'k')) // 5