Defining Functions
You’ve likely learned about functions at some point in algebra class. The idea behind a function like \( y = f(x) = 2x + 1 \) is that you give the function an input value \(x\), and it returns a corresponding output value \(y\). For instance, given the value \(x = 4 \), we compute \(y = f(4)= (2\times 4)+1 = 9\).
Functions play essentially the same role in programming. You give a function some input, and get some output back in return. The notation for defining and evaluating a function in JavaScript follows a similar pattern to mathematics. Here is how you might implement the math function above in JavaScript:
var y = function (x) { return 2*x + 1; }; console.log(y(4)); // 9
Before we go into detail about functions, take a moment to appreciate how similar functions are in mathematics and programming.
Anatomy of a Function
How do we define a function? The diagram below illustrates the basic format.
Generally, we create functions that take one or more inputs and return an output. However, you can also create functions that perform some other action; alert()
, for example, displays the input in a dialog box. You can create other functions that don’t take any inputs, and yet others that take neither inputs nor outputs.
Suppose we wanted to create a function that took a temperature in Celsius and returned it in Fahrenheit, following the formula \(F = \frac{9}{5} C + 32\).
function celsiusToFahrenheitOne(c) { var f = ((9 / 5) * c) + 32; return f; } function celsiusToFahrenheitTwo(c) { return ((9 / 5) * c) + 32; } function celsiusToFahrenheitThree(c) { console.log((9 / 5) * c + 32); } function celsiusToFahrenheitFour(c) { ((9 / 5) * c) + 32; } console.log(celsiusToFahrenheitTwo(22)); // 71.6 console.log(celsiusToFahrenheitTwo(22)); // 71.6 celsiusToFahrenheitThree(22); // 71.6 console.log(celsiusToFahrenheitFour(22) + " "); // undefined
The number of input values you provide does not need to match the number of named arguments in the function definition. If you use an argument inside the function that doesn’t have a corresponding input value however, it is likely to cause an error.
function line(x) { return 2 * x + 1; } // demonstrates variable number usage of arguments // also multiple return levels function verifyLine(x, y) { if (y === undefined) { return "if x=" + x + ", then y=" + line(x); } else { // use a ternary to decide inline whether on line or not return "x=" + x + ", y=" + y + " is " + ((y === 2 * x + 1) ? "on " : "not on ") + "the line"; } } console.log(verifyLine(4, 9)); // x=4, y=9 is on the line console.log(verifyLine(3, 5)); // x=3, y=5 is not on the line console.log(verifyLine(4)); // if x=4, then y=9
The inputs can be any values including, as we will learn, compound data structures such as arrays, objects, and even other functions. The types of inputs can vary from one function call to another. The output can likewise be any value, but only a single data value can be returned, whether it be a simple or compound data structure.
Using conditionals such as if
statements, it is possible to have multiple return
statements in a function. The first return
that is evaluated ends the execution of the function. This can even occur in the middle of a loop, immediately terminating the loop. If no return
statement is found before the function ends, undefined
is the returned value.
// returning inside loops, different output types function lookingForFour(x) { for (var i = x; i < 6; i += 1) { console.log("for loop: " + i); // this will terminate the loop prematurely if i reaches 4 if (i === 4) { return "4 has been found"; } } return false; } console.log(lookingForFour(3)); // 4 has been found console.log(lookingForFour(1.73)); // false console.log(lookingForFour(7)); // false
Appropriately used, functions can fill nearly any role. As a beginner, you’ll start by creating functions as single-purpose tools. Over time, you’ll learn how to create more sophisticated functions that act more as a Swiss Army knife, serving different roles depending on the context. The popular JavaScript library jQuery, for instance, is itself a single function. The ability for functions to accept a variable number of arguments and conditionally set its return statement hints at this potential.
Functions as Data
In a number of programming languages, functions are distinct from data. This is not the case in JavaScript, where you can and should treat functions as data. This means you can assign functions to variables, store them in compound data structures, pass them as parameters into other functions, and even pass them into themselves. As we will see repeatedly, this is a powerful feature of the language.
In the following example, we begin by creating a function with the variable line
pointing to it. We then instruct the variable sameline
to point to that same function. For the variable diffline
, we create a new function. Mathematically, both functions are the same. But in JavaScript, two functions are equal only when they are the same object in memory. So while both line
and diffline
have identical outputs for identical inputs, they are not identical as functions. Only when we assign line
to point to diffline
‘s function do they become equal. At this point, line
and sameline
cease to be equal.
var line = function(x) { return 2 * x + 1; }; var sameline = line; var diffline = function(x) { return 2 * x + 1; }; console.log(line(4), sameline(4), diffline(4)); // 9, 9, 9 console.log(line === sameline, line != diffline); // true, true line = diffline; console.log(line === diffline, line !== sameline); // true, true
Functions need not even have a variable or identifier pointing to them. There are instances where you’ll want a function executed immediately, exactly once. In this case, there is no need to link the function to a variable or give it an identifier. Here is an example:
console.log((function(me) { return me; })("I don't exist!"));
Why would one want to do that? And what happened to the internal name of that function? Read on.
Functions as Parameters
When calling a function, we can pass into it any data we like, including functions. Here’s an example:
var oper = function(a, b, op) { console.log(op(a, b)); }; var mult = function(i, j) { return i * j; }; var add = function(x, y) { return x + y; }; oper(2, 3, mult); // 6 oper(2, 3, add); // 5
This code creates a general operation function, oper
, which then takes in a function op
to operate on the first two arguments a
and b
. This allows one to deal with generic behavior, such as checking that the inputs are numbers and deciding what the output should be, in one function and then having separate, smaller functions deal with more specific behaviors. Think of oper
as a drill while mult
and add
are different drill bits.
Another use of this technique, and one quite common in the web world, is to tell another function what to do when some event occurs. The following example uses the native browser function setTimeout
to set a timer that waits for 1000 milliseconds before calling whatever function was passed into it. The called function receives no input, and any return values are discarded. In the first line, we pass a literal definition of a function, but in the last line, we pass in a variable pointing to a function. Make sure to pass the function, and not an invocation of the function (i.e., fun
, not fun()
), unless fun
‘s return value is a function that you want to pass in.
setTimeout(function(a) { console.log("hi", a); }, 1000); again = function() { console.log("bye"); return "wow!" }; // setTimeout returns an ID that can be used to cancel the timer. // notice how it runs first before either timer fires. console.log(setTimeout(again, 1000));
Note that setTimeout
is itself a function whose return value is a numeric identifier that can be passed to clearTimeout
to cancel the timer. In the example, notice how the numeric identifier is returned immediately, but one has to wait 1 second to see the function again
run.
See the MDC setTimeout and setInterval pages for more details on these functions.
Scope
A big weakness of JavaScript is that variables can be easily used in conflicting ways. The resulting bugs can be obscure and difficult to troubleshoot, to say the least.
i = 5; console.log(i); // 5 for (i = 0; i < 3; i += 1) { console.log(i); } // is the value of i set to 5 or 3? console.log(i); // 3
What was the issue here? Well, we think of the variable i
in one context, then we use it as dummy variable as an index in a for
loop, not considering that we just clobbered the original i
. One way to avoid these situations is to follow good naming conventions (e.g., avoid i
and other short names for any variables that persist). Give names meaning.
Another way is to use scope. Every function has its own scope. This means that a variable declared with var
inside a function is only visible within the function. It is a local variable. Scopes are nested so that when a variable is used, it first checks the most immediate scope and then works its way up the hierarchy. The top scope level is called the global space. If you do not explicitly declare a variable inside a function with the var
keyword, it defaults to referencing the global variable with that name.
i = 5; console.log(i); // 5 (function() { var i; for (i = 0; i < 3; i += 1) { console.log(i); } })(); // with var, is the value of i set to 5 or 3? console.log(i); // 5 i = 5; console.log(i); // 5 (function() { for (i = 0; i < 3; i += 1) { console.log(i); } })(); // without var, is the value of i set to 5 or 3? console.log(i); // 3
To declare a variable as local is to use the keyword var
. This should happen only once, but it can happen anywhere within a function. JavaScript scans the entire function before deciding whether something is a local variable. But be kind to the reader of the code (hint: that’s probably you!) and declare local variables at the top of the function, even if no assignments are made at that point.
JSLint is a great tool for catching global variables and otherwise debugging your code. You can tell JSLint which variables you have made global intentionally, and it will report any global variables you’ve overlooked. Try it out with this poorly written code:
// var statement can even appear in a body of loop, but don't do this! i = 5; console.log(i); // 5 (function() { for (i = 0; i < 3; i += 1) { var i; console.log(i); } })() console.log(i); // 5
Remember that it’s good practice to declare all variables at the top of the relevant scope:
var temp = function() { var a, b = 4, c; a = 4 * b; c = a + b; return c; } console.log(temp()); // 20
You can declare variables in a single line separated by commas, and you can optionally assign values to them. Here, b
receives a value but a
and c
do not.
Side Effects
Mathematical functions take input and give output, and that’s all they do. Programming functions can be used like that, and many argue that they should only be used this way. But it is possible for a JavaScript function to have side effects, which modifies data elsewhere beyond that function.
c = 0; // this function does not return a value, // but it does change the global variable c function countBy(inc) { c += inc; } while (c < 10) { countBy(2); console.log(c); }
Global variables and compound data structures, like objects and arrays, are common scenarios for creating side effects. When objects and arrays are passed to a function, their contents can be modified and those modifications live on, whether they’re returned by the function or not.
Some functions are created solely for their side effects, but many side effects are created accidentally. It is important to recognize any side effects caused by your functions. Carefully consider the intentional use of side effects and document them.
Closures
Closures are another topic related to scope. In a nutshell, closures enable local variables to exist for as long as needed.
If you define a function within another function, then the inner function can access variables local to the outer function, so long as they’re not replaced by variables local to the inner function. This is the same process that gives access to global variables from within a function, following the scope chain from the most local scope up to the global scope.
If the inner function exists beyond the execution of the outer function, any variables of the outer function that need to exist will remain in existence while the inner function exists. This space is shared by all functions that have tapped into the scope of the outer function. It is a new, exclusive global space for the inner functions.
And that global space spawns anew each time the outer function runs.
var makeLine = function(slope, u, v) { // compute intercept var intercept = v - slope * u; // make function and return it return function(x) { return slope * x + intercept; }; }; lineA = makeLine(2, 0, 1); console.log(lineA(4)); // 9 lineB = makeLine(-3, 2, 4); console.log(lineA(4)); // 9 console.log(lineB(4)); // -2
The function makeLine
takes in a slope and a point on a line; it returns the unique linear function that is defined by these values. Both the intercept
and the slope
are local variables of makeLine
that have to persist for the sake of the returned line function. And so JavaScript keeps them alive and accesses their values when the line function is called. Creating a second line creates a new slope
and intercept
for the new line, without interfering with the old line.
Let us modify this so that we have access to the variables.
var makeLine = function(slope, u, v) { // compute intercept var intercept = v - slope * u; // make a global function that returns slope and intercept inspector = function() { console.log(slope, intercept); } // make a global function that can change this slope manipulator = function(a) { slope = a; }; // make function and return it return function(x) { return slope * x + intercept; }; } var lineA = makeLine(2, 0, 1); console.log(lineA(4)); // 9 inspector(); // 2, 1 manipulator(3); // changes the slope in the already made line inspector(); // 3, 1 console.log(lineA(4)); // 13 oldinspect = inspector; var lineB = makeLine(-3, 2, 4); // this is a new inspector function inspector(); // -3, 10 // the old one still lives on! oldinspect(); // 3, 1
We have added an inspection function and a manipulation function that can be used to interact with the closed over slope
and intercept
variables. These examples demonstrate that these should still be thought of as variables and not copies of the original.
This example also illustrates that:
// the outer function exists purely to give closure var inn = (function() { var clo = 3; var toReturn = function() { clo += 1; return clo; }; clo = 7; return toReturn; })(); console.log(inn()); // 8 console.log(inn()); // 9
When declaring the function toReturn
, the value of clo
at that point is irrelevant. But whenever it is invoked, it can manipulate clo
as if it were any other variable in its scope.
A Function By Any Other Name
Functions can be defined in two basic ways. By assigning it to a variable:
var anyOldName = function(input) { // code }
Or by giving it an internal name:
function myName(input) { // code }
What is the difference? Consider how functions are called. To call a function, it needs to be stored in a variable as in the first example and accessed through that variable (e.g., anyOldName();
), or wrapped in parentheses and invoked immediately (e.g., (function(input){alert('hi');})()
).
In the second example, the internal name myName
is not used for this purpose, except internally (i.e., when the function references itself). The internal-name-only statement is roughly interpreted as:
var myName = function myName(inputs) { // code }
In essence, JavaScript creates a local variable with the same name as the internal name. But that creation and assignment happens at the top of the scope. This has unpleasant implications.
The best practice is to avoid the internal-name only function declaration. Be explicit where you are storing the function. If you need the internal name, then you can include it.
If you’d like to explore this messy issue further, we have a fiddle for exploration. To demonstrate the errors in this exploratory fiddle, we use the try
statement. try
is also useful when trying to isolate errors in your own code. See the MDC try catch reference page for more details.
Using a Function’s Internal Name
Why do functions have an internal name then, that the outside world mostly cannot see? Well, suppose that you need to reference the function from within itself. If you use the external name, you need to know that name from inside the function. If the external name, that variable, is assigned a new value, then the function will be referencing the value instead of itself. Internal names are a way to ensure that does not happen. An internal name cannot be changed once assigned and the same internal name may be used for various functions without risk of confusion.
Why would a function need to access itself? There are two common cases. First, the function may call itself – a technique known as recursion. In the following code, the function calls itself using its internal name of primeFactors
. Be careful with recursion not to call the function itself infinitely.
var pf = function primeFactors(n, p) { // check to see that n is evenly divisible by p if (n % p === 0) { // divide by p and continue g = primeFactors(n / p, p); return g + 1; } else { return 0; } }; console.log(pf(18, 3)); // 2 console.log(pf(16, 2)); // 4
The second common case is when the function wants to access or store some data in itself. This is possible because functions are objects, and we can store data in objects. Continue to Making Associations to learn more.
Leave a Reply
You must be logged in to post a comment.
Trackbacks & Pingbacks