JavaScript functions

Creating our own functions

In our discussion of JavaScript fundamentals, we encountered several “built-in functions,” written into the JavaScript and D3 languages, that we were able to invoke upon command for different purposes. For example, in plain JavaScript, we found that the pattern Math.random() gives us a random number between 0 and 1; in D3, the pattern d3.select().html() allows us to grab an element in the document and replace its contents. These functional patterns are like more building blocks we can add to our toolkit of variables and expressions to extend what we can do in the language; for many common things we will want to do with code, we will find a pattern like these that we can leverage.

Sometimes, however, we will want to do something specific that a built-in function cannot do. In other cases, we will want to do something more complex for which there simply isn’t any built-in pattern. In these situations, we will want to create our own functions, and that is the focus of this week’s demonstration.

User-defined functions

A user-defined function is simply a function that we define, as the person writing the code, to accomplish a specific task. In the vast majority of cases, we will define it because there isn’t a named pattern built into the language we are using that will do the thing we want to do right out of the box.

In previous discussion, we’ve used the word “function” loosely, to describe in general terms a pattern of code that behaves (or functions) a certain way. But this general usage of the term is imprecise and will cause us some difficulty when we learn other concepts with potentially conflicting vocabulary. (As an aside, the things we have been calling built-in “functions” are technically called “methods,” but we haven’t gotten to the point in our exploration to understand what this distinction is!) For this reason, we will use this term in a very specific way from this point forward. Specifically, we will say that a function is a reusable block of code that performs a specific task. That specific task can be many different things — generating a random number, printing a greeting on the screen, or drawing a bar chart with a new data set. Whatever it is, we will use the function to abstract what we are trying to do into something more generalizable. Think back to the metaphor of the cake recipe, in which there were “high-level abstractions” — such as “mix dry ingredients” — that were part of our procedure. Remember that “mix dry ingredients” was actually a collection of many smaller subtasks that we did not need to explicitly specify, such as retrieving the ingredients and placing them in a bowl one by one. When writing a recipe, it is more efficient to group all of these subtasks together into a single step; likewise, when writing code, it will sometimes be more efficient to group together several related steps to reinforce their importance as a collective procedure.

Once we have these higher-level abstractions, we can reuse them in many places. And this is really the greatest advantage of creating functions — having the ability to take a complex task we need to perform, group that complex task together by defining it through a function, and then repeat that complex task in many places in our code by invoking the function we have defined. A function is a reusable block of code for precisely this purpose, and it will help clean our code, eliminate redundancies, minimize the possibility of errors, and make our code more efficient.

Varieties of functions

In JavaScript, there are a couple of different ways we can create and use functions, but the open structure of such functions means that a basic pattern can be used to perform a multitude of different, complex actions.

In some cases, we will want to do a complex action through a function and then give a name to that function so that we can invoke it in many places in our code. We will call such functions named functions, or function definitions. In other cases, we will want to use a function that we create only temporarily and have that function be useful for the one part of our code in which we are using it. In such cases, we may not want to give a name to our function, and we will call such patterns function expressions or anonymous functions.

In call cases, a function represents a process, much like the cake-baking process of our recipe metaphor. Recall that in that metaphor, the process represented by the steps in the recipe required some inputs (i.e., ingredients and equipment) and yielded an output (i.e., the cake). Whenever we create a function in JavaScript, we can optionally pass into our defined process some inputs that will be used in that process. Likewise, on the other side of the function, we may get some sort of output back, which could be a literal value we capture through the help of a variable, or something more indirect like a state change in the page itself. This inputs → process → outputs model will help us understand what these functions are doing and why we would want to use them in the first place.

Function definitions

Let’s begin by exploring named functions. A named function is simply a function we define with a name that we choose. This name will behave very similarly to the name we give a variable — whatever name we choose, we will be able to refer to that function by the same name anywhere in our code. We also refer to a named function as a function definition, because we are defining a new function we create through the assistance of a name we choose.

A function definition will always look like the following pattern:

function NAME(PARAMETERS) {
    FUNCTION BODY
}

The word function out front is another reserved keyword in JavaScript, much like let and const for creating variables. This is a word in the JavaScript language, and it declares “I am about to define a function!” (Sound familiar?) After this keyword, we provide a name for our new function we are creating. Just like variable names, this function name can be anything we want — but within limits. Naming rules for functions follow the same rules for variables, in terms of what characters are allowed or not. Within those constraints, we can create any name that we want, but we will want to give our function a name that makes sense and is indicative of what the function does. For example, a name like addNumbers clearly tells us that the function likely performs some sort of addition; meanwhile, a name like anAwesomeFunction doesn’t tell us anything at all about what the function does. We should choose our names wisely.

Immediately after the name of the function, we have open and closed parentheses ( ), followed by open and closed curly braces { }. Within the parentheses, we can have one or more parameters, which are names that we choose that will behave like variables inside the function, or as inputs to the process wrapped in the function. But we aren’t required to have parameters when we define a function if we don’t need them, such as when the process we are capturing in a function doesn’t require any inputs. In such situations, the parentheses will still be there but will be empty.

Inside the curly braces, we will place whatever code we want to run every time we invoke the function by name. If we want to generate a random number, we will put the code required to do that within this block; if we want to generate a random number, multiply it by 100, and then use the resulting value to draw a certain number of cat drawings on the screen, then the code required to perform this sequence of steps would likewise appear inside the curly braces. There is no minimum or maximum number of lines of code that must appear inside the braces, and we can even leave the function empty if we really want to (though that wouldn’t be very useful at all).

Consider the following function definition:

function sayHello() {
    console.log("Hello, world!");
}

The name of this function is sayHello. This particular function has no parameters, so the parentheses are empty (but still present). Inside the curly braces, there is a console.log() statement that prints a greeting to the JavaScript console ("Hello, world!"). Every time we invoke this function, this greeting will be printed to the console, however many times we call it by name.

Importantly, the above code is merely a function definition. When we define a function by name, it does not automatically run by itself. It simply represents a sequence of commands to execute, if and only if, or when and only when, we ask the browser to initiate the sequence. Until then, the code that we place inside the curly braces does not run.

How do we get this code to run? Through a function call. This means calling a function by name elsewhere in the code, similar to how we might refer to a variable by name elsewhere in our code. When we do this, we must refer to the function by the exact same name (with correct spelling and capitalization), and we must include the parentheses, along with any input values (if necessary). For example, to call the above function, we would write this:

function sayHello() {
    console.log("Hello, world!");
}

sayHello(); ← Function Call

This call is on its own line, much like console.log(). When the browser is interpreting our JavaScript and gets to this line, it will see this name and know that it needs to initiate the sequence of commands that are written inside the function by the same name.

In the above example, we don’t have any parameters (i.e., there's nothing inside the parentheses). But this is only because the process we are encapsulating within the function doesn’t require any input values. When we do have input values that we need to supply to the function, we will represent them through the parameters themselves.

These things called parameters are often the most confusing aspect of functions. When we include them in a function definition, we are in essence saying that any time the function is called, it must be simultaneously supplied with some input values. Those input values will be given certain names, which we define through the names of the parameters. Inside the curly braces for that function, the parameters will behave like variables, whose values are determined by the inputs supplied with the function call. Accordingly, we will be able to refer to the parameters by name inside the function exactly the same way we refer to variables we declare elsewhere.

Consider the following example, which has one parameter defined:

function sayHello(yourName) {
    console.log(`Hello, ${yourName}!`);
}

The name of this function is sayHello, just like the previous example. But this version of the function has a single parameter, whose name is yourName. Inside this function, yourName is used like a variable to construct a template literal. This template literal creates a complex String that gets printed to the console. (Note: just like variable names, we get to decide what the names of these parameters are. We should use parameter names that would make sense as variable names, as demonstrated in this example.)

When we then call this function, we must supply it with whatever the value of yourName should be. To do this, we pass in a value inside the parentheses of the function call, like this:

sayHello("Steven");

Inside a function call, this value inside the parentheses is called an argument. Note that this is different from how we used the word “parameter” above! The things inside the parentheses of a function definition are called parameters, but the the values inside the parentheses of a function call are called arguments. When we pass an argument into a function, the value of that argument (e.g., the String value "Steven") gets assigned to the corresponding name of the parameter (e.g., yourName), and that parameter name will then be used inside the function definition to be equal to the value of the argument. In other words, when we call this function, inside the curly braces, we now have an implied variable named yourName whose value is "Steven".

Argument values are assigned to parameter names in the order in which they are supplied. For example, if we have a function definition with two parameters, then the function will expect to receive values for those parameters in the same order at the time the function is called:

function sayHello(yourName, yourColor) {
    console.log(`Hello, ${yourName}! Your favorite color is ${yourColor}.`);
}

sayHello("Steven", "blue");

In this function, we have two parameters named yourName and yourColor. These parameters are listed in order and separated by commas; if we need more parameters, we can add to this list inside the parentheses, and separate additional parameters by additional commas. When we call this function, we are passing in two arguments, which are the String values "Steven" and "blue". The code inside the curly braces of the function definition then gets run, and in that process, we now have two variable-like names we can use: one named yourName, whose value is "Steven", and one named yourColor, whose value is "blue".

Variable Scope

These parameters in a function definition behave like variables, but they only exist for the duration of the code block inside the curly braces. We can refer to them by name when inside the curly braces, but as soon as we exit the curly braces — as soon as all the code inside the function definition has run — those names no longer become accessible. We can confirm this by attempting to print the value of yourName to the console inside and outside the function definition:

function sayHello(yourName, yourColor) {
    console.log(yourName);
    console.log(`Hello, ${yourName}! Your favorite color is ${yourColor}.`);
}

sayHello("Steven", "blue");
console.log(yourName); ← Undefined!

Notice that the console.log() statement inside the function definition correctly prints out the value of yourName as "Steven". However, the console.log() statement outside the function definition gives us an error — this variable is undefined!

This difference in accessibility of this value, based on location in our code, is due to a phenomenon called scoping. The scope of a value refers to the regions of code in our script in which that value can be accessed, used, and referred to by name. In previous demonstrations, all of the variables we have declared have been globally scoped, meaning they can be used anywhere in our code; no matter where we refer to them by name, the browser will be able to access their value. In contrast, things like parameters in a function definition are locally scoped, meaning they are only accessible within the code block of the function itself (i.e., inside the curly braces of the function definition).

Variables that have local scope cannot be used in a global context. However, variables that have global scope can be used in a local context. In other words, globally scoped variables can be used anywhere, but locally scoped variables are limited or restricted in where they can be used.

Scoping rules, which dictate when and where variables can be used, are present everywhere in our code, and they have major consequences for whether or not our code works as expected. For instance, sometimes we will be trying to use a variable that has local scope outside of its valid region of accessibility without realizing it. This most commonly happens when we define a new variable inside of a function and then attempt to use the same variable by name outside of our code, such as in this example:

function sayHello(yourName, yourColor) {
    let message = `Hello, ${yourName}! Your favorite color is ${yourColor}.`;
}

sayHello("Steven", "blue");
console.log(message); ← Undefined!

So then what happens if we want to use a value of something that is locally scoped in a global context, outside of the function definition? We have a couple of options. The first of these is to define the variable we want to use both inside and outside a function definition as a global variable, like this:

let message;
function sayHello(yourName, yourColor) {
    message = `Hello, ${yourName}! Your favorite color is ${yourColor}.`;
}

sayHello("Steven", "blue");
console.log(message); ← This works!

In this example, the variable named message is declared outside the function definition, which means we can modify or access its value both inside and outside the function. Notice that inside the function definition, the variable message doesn't get re-declared with let but instead gets assigned a new value; when we call the function, and then later console.log() the value of message, we see the modified value of the variable printed to the console.

The second option is to use what is called a return statement. When we use the return keyword in front of a value inside of a function definition, or in front of an expression or variable that evaluates to a value, we effectively eject that value back outside the function so that we can use it in other places. This is especially useful, for example, when we declare a new variable inside of a function definition (which is thus locally scoped) and then want to use its value elsewhere in our code. The following example demonstrates this:

function sayHello(yourName, yourColor) {
    let message = `Hello, ${yourName}! Your favorite color is ${yourColor}.`;
    return message;
}

When we use the return keyword, we must have a way of capturing the value that gets ejected. The way we do this is by assigning the output of the function call to a new variable, like this:

function sayHello(yourName, yourColor) {
    let message = `Hello, ${yourName}! Your favorite color is ${yourColor}.`;
    return message;
}

let newMessage = sayHello("Steven", "blue");

Notice what is happening in the line above: we have a normal function call, as we’ve seen before, but instead of having that function call happen separately on its own, we are capturing the output value that is yielded from that function (through the return statement) in a new variable we declare, named newMessage. We can now use that returned value anywhere else, since it is now stored in a globally-scoped variable.

Note that this only works when we have a function with a return statement. A function only yields a capturable output when it has a return statement inside of it, meaning that if we try to assign a function’s output to a variable when it doesn’t have a return statement, that new variable we create won’t have any useful value to use.

Also note that the return statement effectively short-circuits a function. As soon as the browser hits a return statement in a function, it returns that value and then stops interpreting any more code inside the function block thereafter. If there is a return statement in a function definition, it must be the very last statement inside the function block. Otherwise, any code that follows it will be ignored and will never run.

Function expressions

The examples above focus on functions with names — they are formal constructions that we can refer to by name many times in a script. But sometimes we don’t want to use a named function and instead just want a quick function to do one thing one time, without us having to go through the formal function definition and call process. In JavaScript, there is another code pattern variation we can use for this purpose: function expressions.

A function expression behaves just like a function definition in terms of accepting some inputs, accomplishing a certain task, and possibly giving back an output. In fact, a function expression is just another way of creating a function that will do a certain task for us. But unlike a function definition, such function expressions don’t have a name. As a result, we call them anonymous functions; we can think of them as transient functions that only exist at the moment we need them.

In our examples, we will use function expressions in larger code constructions, typically wrapped inside some parentheses as an argument for another function. This can be difficult to wrap our heads around, so let’s consider an example.

A common task in JavaScript is to ask the browser to do something when a user clicks on a button, such as the button in a form. In plain JavaScript, we call this kind of task the process of adding an event listener, which is a fancy way of saying “listen for a specific kind of event to happen, and when it does happen, do something specific in response.” There are many ways of adding such event listeners, but in D3, there’s a shorthand that we will be able to use that looks like the following:

d3.select(ELEMENT).on("click", function() {
    FUNCTION BODY
});

The d3.select() should look familiar; it is the same code pattern we have seen when drawing shapes and replacing the contents of HTML elements. In this context, we are using this pattern to grab (or “select”) a specific element of interest, and then with the .on() part, we are adding an event listener for the “click” event. Notice that inside the parentheses, there are two arguments: the name of the event we are listening for is listed first ("click"), and the second is what we want to have happen when the event occurs.

In this example, the second argument is an example of a function expression. It uses the same function keyword as our named functions above, along with parentheses and curly braces, but unlike our previous function definitions, this function doesn’t have a name. This is a subtle difference, but it is an important one! The whole point of naming a function before was so that we could invoke it on multiple occasions. With this example, we can only invoke the function where it is located in our code. Specifically, here, this function will be invoked if and when the element we’ve selected is clicked, and every time thereafter it is clicked.

A function expression has the same features as a function definition in terms of optional parameters, scoping limitations, and use of return statements. Because of that, you might be wondering what the advantage of using a function expression is over a named function. At this point, we don’t have enough information to answer this question fully, but in future weeks, we will see that we’ll need to use function expressions within larger and more complex code structures, in both plain JavaScript and D3 alike. For the time being, the most important thing to remember is that different varieties of function syntax will be useful in different situations, and those situations will be revealed in the near future.