3. Functions

Objective: By the end of this checkpoint you will understand Functions in JavaScript and will be able to use them when needed.

Key Terms

  • function
  • parameters
  • Bindings
  • Scopes
  • Global
  • Local
  • Arrow Functions
  • Call Stack

JavaScript Functions

Functions are the bread and butter of JavaScript programming. The concept of wrapping a piece of program in a value has many uses. It gives us a way to structure larger programs, reduce repetition, associate names with subprograms, and isolate these subprograms from each other.

The most obvious application of functions is defining new vocabulary. Creating new words in prose is usually bad style. But in programming, it is indispensable.

Typical adult English speakers have some 20,000 words in their vocabulary. Few programming languages come with 20,000 built-in commands. And the vocabulary that is available tends to be more precisely defined, and thus less flexible, than in human language. Therefore, we usually have to introduce new concepts to avoid repeating ourselves too much.

 

Defining a function

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:


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.

The function body of a function created this way must always be wrapped in curly braces ({}), even when it consists of only a single statement.

A function can have multiple parameters or no parameters at all. In the following example, the makeNoise function does not list any parameter names, whereas the power function lists two:

Some functions produce a value, such as the square and power functions we presented above, and others don’t, such as the makeNoise function, whose only result is a side effect. 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 the makeNoise function, similarly return undefined.

Parameters in 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 that 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. This provides some isolation between functions — each function call acts in its own little world (its local environment) and can often be understood without knowing a lot about what’s going on in the global environment.

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 (i.e., ES6), only functions created new scopes. So in old-style bindings, created with the var keyword, if the variables are not in a function, then they are visible not only throughout the whole function that they appear in, but also throughout the global scope.

Each scope can “look out” into the scope around it, so x is visible inside the block in this example. The exception is when multiple bindings have the same name — in that case, code can see only the innermost one. For instance, in the following example, when the code inside the halve function refers to n, it is seeing its own n, not the global n.

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 below which outputs the ingredients needed to make a batch of hummus, has another function inside of it:

The code inside the ingredient function can see the factor binding from the outer function. But its local bindings, such as unit or ingredientAmount, are not visible in the outer function.

The set of bindings visible inside a block is determined by the place of that block in the program text. Each local scope can also see all the local scopes that contain it and all scopes can see the global scope. This approach to binding visibility is called lexical scoping.

 

Functions as Values

A function binding usually simply acts as a name for a specific piece of the program. Such a binding is defined once and never changed. This makes it easy to confuse the function and its name. But the two are different. 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 this:

Declaration Notation

There is a slightly shorter way to create a function binding. When the function keyword is used at the start of a statement, it works differently. For example:

This is a function declaration. The statement defines the binding 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. Take a look at the example below:

The preceding code works, even though the function is defined after 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 as >=).

The arrow comes after the parameters in the parenthesis and is followed by the function’s body. It expresses something like “this input (the parameters) produces this result (the body)”.

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:

When an arrow function has no parameters at all, its parameter list is just an empty set of parentheses. For example:

There’s no deep reason to have both arrow functions and function expressions in the language. Arrow functions were added in 2015, mostly to make it possible to write small function expressions in a less verbose way.

The Call Stack

The way control flows through functions is somewhat involved. Let’s take a closer look into it. Here is a simple program that makes a few function calls:

A run through of this program goes roughly like this: the call to greet causes control to jump to the start of that function (line 2). The function calls console.log, which takes control, does its job, and then returns control to line 2. There it reaches the end of the greet function, so it returns to the place that called it, which is line 4. The line after that calls console.log again. After that returns, the program reaches its end.

We could show the flow of control schematically like this:

  1. not in function
  2. in greet
  3. in console.log
  4. in greet
  5. not in function
  6. in console.log
  7. not in function

Since 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 cycle between two functions.

Optional Arguments

The following code is allowed and executes without any problem:

We defined square with only one parameter. Yet when we call it with three parameters, JavaScript doesn’t complain. It ignores the extra arguments and computes the square of the first parameter.

JavaScript is extremely broad-minded about the number of arguments we pass to a function. If we pass in too many, the extra ones are ignored. If we pass in too few, the missing parameters get assigned the value undefined.

The downside of this is that you’ll likely pass in the wrong number of arguments to functions. And no one will tell you about it.

The upside is that this behavior can be used to allow a function to be called with different numbers of arguments.

For example, this minus function tries to imitate the subtraction, -, operator by acting on either one or two arguments:

If you write an = operator after a parameter, followed by an expression, the value of that expression will replace the argument when it is not given.

For example, this version of power function below makes the second argument optional. If you don’t provide it or pass the value undefined, it will default to two, and the function will behave like squaring a number.

In the upcoming checkpoints, we will see a way in which a function body can get at the whole list of arguments it was passed. This is helpful because that makes it possible for a function to accept any number of arguments. For example,