You'll often have some code that you need to execute multiple times in a program. You may need to run it 2 or 3 times, or you may need to run it millions of times. Regardless, you don't want to write that code repeatedly. What can you do?
One option is to use loops -- we'll discuss them later -- but they aren't always helpful. Instead, most languages have a structure called a procedure that lets you extract the code and run it as a separate unit. In JavaScript, we call these procedures functions.
Before using a function, you must first define it. A typical function definition looks like this:
function funcName() {
func_body;
}
Here, the funcName
is the name you want to give to the function, and func_body
is some code that performs the action(s) required by the function. There are other ways to define functions; we'll discuss those a little later.
Here's a function named say
:
function say() {
console.log("Hi!");
}
Why do we want a function named say
? To say something, of course! Let's try it. First, we'll create a file named say.js
, then place the following code inside the file.
function say() {
console.log("Output from say()");
}
console.log("First");
say();
console.log("Last");
Go ahead and save the file, then run it from the terminal using the following command:
node say.js
It should display the following output on the console:
First
Output from say()
Last
On line 6 of say.js
, the code say()
is described as a function call to the say
function. When JavaScript runs this program, it creates a function named say
whose body causes JavaScript to print the text Output from say()
when the function executes. However, that doesn't happen just yet.
On line 5, we use the console.log
method to print First
to the console. On line 6, we then call the function say
by appending a pair of parentheses -- ()
-- to the say
function's name. This causes JavaScript to temporarily jump into the body of the say
function. At this time, it executes console.log("Output from say()")
to print the text Output from say()
on the console.
Finally, JavaScript returns to the code that immediately follows the call to say
and logs Last
to the console.
Note that the parentheses on line 6 -- ()
-- make this code a function call. Without the parentheses, say
doesn't do anything useful. It's just the function's name; it is a variable whose value is a "function object." We'll learn about function objects later in the Core Curriculum.
Did you notice that console.log
also looks like a function call? It is, in fact, a function call, though we typically call it a method call instead. The .
in console.log
distinguishes it as a method call instead of a function call. We'll talk about the differences shortly.
Programmers often used the words invocation and invoking when talking about function and method calls. The terms are synonymous with "call" and "calling" but more clearly distinguish between the noun and verb forms. Invocation is a noun while invoking (as well as invoke and invoked) are the verb forms. You invoke a function to call it or write a function invocation that will be called when the program runs. We also use these terms, especially when we need to avoid ambiguity.
Create a new file named say2.js
with the following code:
console.log("hello");
console.log("hi");
console.log("how do you do");
console.log("Quite all right");
Go ahead and run it if you want. Notice how we've duplicated the console.log
call several times. Instead of calling it every time we want to output something, we want to have code we can call when we need to log something.
Now, let's update say2.js
as follows:
function say(text) {
console.log(text);
}
say("hello");
say("hi");
say("how do you do");
say("Quite all right");
At first glance, this program seems a bit silly. It doesn't reduce the amount of code. In fact, it adds more. Also, the say
function doesn't provide any functionality that console.log
doesn't. However, there is a benefit here. We've extracted the logic for displaying text in a way that makes our program more flexible. If we later need to change the output style, we can make the change in one place: the say
function. We don't have to change any other code to get the updated behavior. We'll see such an update in a few minutes.
Here, we only needed to modify line 2 - we didn't have to alter all the strings.
As we saw earlier, functions are invoked by typing their name followed by a pair of parentheses. say2.js
illustrates how we can define and call a function that takes a value known as an argument. Arguments let you pass data from outside the function's scope into the function so it can access the data. You don't need arguments if the function definition doesn't need access to outside data.
In say2.js
, the function definition includes (text)
after the function name. This syntax tells us that we should supply (pass) a single argument to the function when we call it. To pass an argument to say
, we include the argument between the parentheses we use to invoke the function. Thus, say("hello")
passes the argument "hello"
to the function. Inside say
, the variable text
is assigned to the argument's value.
In the definition of a function, the names between parentheses are called parameters. You can think of parameters as placeholders, while arguments refer to the values assigned to those placeholders.
Function names and parameters are both considered variable names in JavaScript. Parameters, in particular, are local variables. They are defined locally within the function's body. Function names are either global or local, depending on whether the function is at the program's top level or nested inside a class, object, or another function. We'll see several exercises about these issues below; they are essential even if there isn't much to say about them.
Arguments are objects or primitive values being passed to the function; parameters are declarations for the local variables used inside the function to access the arguments.
Many developers conflate the terms "parameter" and "argument" and use them interchangeably. The difference is typically not crucial to understanding, but using the correct terminology is still a good idea. We will expect you to do so on Launch School assessments.
It's also important to realize that function names and parameters are variable names in JavaScript. Function names are usually global variables, but they can also be local variables if the function is nested inside another function. Parameters, however, are local variables only available within the function's body.
You can name functions and parameters with any valid variable name. However, you should use meaningful, explicit names that follow the same naming conventions discussed earlier. In the above example, we name the parameter text
since the say
function expects the caller to pass in some text. Other suitable names might be sentence
or message
.
When we call the say
function, we should provide a value -- the argument -- to assign to the text
parameter. For instance, on line #5 of say2.js
, we pass the value "hello"
to the function, which it uses to initialize text
. The function can use the value in any way it requires. It can even ignore the value entirely. To repeat an earlier item, the scope of the variable defined by the parameter is the function definition; you can't use it outside the function's body.
Functions can be defined with any number of parameters and may be passed any number of arguments. For instance, here's a function defined with 2 parameters and invoked with 2 arguments:
function add(left, right) { // left & right are parameters here
let sum = left + right; // left & right are arguments here
return sum;
}
let sum = add(3, 6); // 3 and 6 are arguments
The additional arguments will be ignored if you provide too many arguments when calling a function. If you provide too few arguments, the parameters and variables that correspond to the missing arguments will receive a value of undefined
:
function add(left, right) { // left & right are parameters here
let sum = left + right; // left & right are arguments here
return sum;
}
console.log(add(3, 6, 5)); // 5 is ignored; prints 9
console.log(add(3)); // second argument missing; prints NaN
// 3 + undefined is NaN
Let's return to say2.js
. When we call say("hello")
, we pass the string "hello"
as the argument that gets associated with the text
parameter. The code within the function definition then executes with the text
local variable assigned to "hello"
.
As mentioned earlier, one benefit that functions give us is the ability to make changes in one location. Suppose we want to add a ==>
to the beginning of every line that say
outputs. All we have to do is change one line of code -- we don't have to change the function invocations:
function say(text) {
#highlight;
console.log("==> " + text);
#endhighlight;
}
say("hello"); // => hello
say("hi"); // => hi
say("how are you"); // => how are you
say("I'm fine"); // => I'm fine
Run this code to see the result. We've now added a ==>
to the beginning of each line. We only had to change a single line of code. Now you're starting to see the power of functions.
So far, all of the functions we've written display text on the console. They can do much more. One common use case is to perform an operation and return a result to the call location for later use. We do that with return values and the return
statement.
All JavaScript function calls evaluate to a value. By default, that value is undefined
; this is the implicit return value of most JavaScript functions. However, when you use a return
statement, you can return a specific value from a function. This is an explicit return value. Outside of the function, there is no distinction between implicit and explicit return values. Still, it's important to remember that all functions return something unless they raise an exception, even if they don't explicitly execute a return
statement.
Let's create an add
function that returns the sum of two numbers:
function add(a, b) {
return a + b;
}
add(2, 3); // returns 5
When you run this program, it doesn't log anything since add
doesn't call console.log
or any other output methods. However, the function does return a value: 5
. When JavaScript encounters the return
statement, it evaluates the expression, terminates the function, and returns the expression's value to the location where we called it.
Let's capture the return value in a variable and log it to the console to verify that:
let twoAndThree = add(2, 3);
console.log(twoAndThree); // => 5
JavaScript uses the return
statement to return a value to the code that invoked the function. The calling function is sometimes referred to as the caller, but this is slightly ambiguous since the caller can also refer to a calling object. If you don't specify a value, it returns undefined
. Either way, the return
statement causes the function to stop running and returns control to the caller.
Functions that always return a boolean value, i.e., true
or false
, are called predicates. You will almost certainly encounter this term in future readings and videos, so commit it to memory.
When you define a function, you sometimes want to structure it so you can call it without an argument. Let's update say
to use a default value when the caller doesn't provide an argument.
function say(text = "hello") {
console.log(text + "!");
}
say("Howdy"); // => Howdy!
say(); // => hello!
You'll notice that say()
-- without arguments -- logs "hello!" to the console. Since we've provided a default value for text
, we can call our function without arguments. Nice!
You can create functions anywhere, even nested inside another function:
function foo() {
function bar() {
console.log("BAR");
}
bar(); // => BAR
bar(); // => BAR
}
foo();
bar(); // ReferenceError: bar is not defined
Here, the bar
function is nested within the foo
function. Such nested functions get created and destroyed every time the outer function runs. (This usually has a negligible effect on performance.) They are also private functions since we can't access a nested function from outside the function where it is defined.
Let's take a few minutes to review the crucial concept of variable scope and learn how scope works with functions. In JavaScript, there are two types of variables based on where they're accessible: global variables and local variables. Global variables are available throughout a program, while local variables are confined to a function or a block.
The keyword you use to declare a variable and the location where you declare it combine to determine whether it is global or local. Ignoring var
for now, we'll focus on let
and const
instead.
As the name suggests, global variables have global scope, which means they are available everywhere within a program. You can read and reassign them at any time. Any variable declared inside a function or block is a local variable - everything else is a global variable.
Let's look at an example. Add the following code to a file named greeting.js
and run it.
let greetingMessage = "Good Morning!";
console.log(greetingMessage);
Since the greetingMessage
variable isn't declared as part of a function definition, the variable's scope is global. Thus, the above code predictably logs 'Good Morning!'
to the console.
Let's change our program slightly by writing a function that uses greetingMessage
:
let greetingMessage = "Good Morning!";
function greetPeople() {
console.log(greetingMessage);
}
greetPeople();
This program produces the same output as before. However, here we use greetingMessage
from within the function. We can do that since greetingMessage
is a global variable, which means we can use it anywhere. We can even reassign global variables from inside a function.
let greetingMessage = "Good Morning!";
function greetPeople() {
console.log(greetingMessage);
}
function changeGreetingMessage(newMessage) {
greetingMessage = newMessage;
}
changeGreetingMessage("Good Evening");
greetPeople(); // => 'Good Evening'
We've added a changeGreetingMessage
function to our program that reassigns greetingMessage
to a new string supplied as an argument. Line 11 invokes the function, passing in the 'Good Evening'
string, which becomes the new value for the global greetingMessage
.
Global variables can be helpful in some scenarios, e.g., application-wide configuration. However, most developers discourage their use since they often lead to bugs. In general, you should limit the scope of your variables as much as possible; smaller scopes limit the risk that an outer scope might misuse the variable.
As the name suggests, local variables in JavaScript have a local scope, meaning that you can't access them outside the function that declares them. As with global variables, where you declare a local variable determines its scope.
Let's see an example of local variables in action.
function greetPeople() {
let greetingMessage = "Good Morning!";
console.log(greetingMessage);
}
greetPeople();
The above code is functionally identical to the first example in the previous section. However, it doesn't use any global variables. The greetPeople
function declares greetingMessage
internally. It's accessible within the function, but a ReferenceError
occurs if you try to use it elsewhere.
function greetPeople() {
let greetingMessage = "Good Morning!";
console.log(greetingMessage);
}
greetPeople();
console.log(greetingMessage); // raises ReferenceError
Try running the above program with node
to see the error message.
This discussion raises a question: how can we ask greetPeople
to output a different greeting message if we've hard-coded our greeting message on line 2? The answer is function arguments, of course:
function greetPeople(greetingMessage) {
console.log(greetingMessage);
}
greetPeople("Good Morning!");
The greetingMessage
parameter acts like a local variable. It is, in fact, a local variable. The chief difference is that we initialize it from the argument passed to the function. Parameters have local scope within a function.
That brings us to another important property of local variables. Local variables are short-lived; they go away when the function corresponding to their scope stops running. When we invoke the function, we start a new scope. If the code within that scope declares a new variable, that variable belongs to the scope. When the last bit of code in that scope finishes running, the locally scoped variables disappear. JavaScript repeats this process each time we invoke a function.
Thus far, we've talked about variables scoped to a function definition. Another way to scope variables locally is to use block-scoping. We've already discussed it in the Variables chapter, and we'll expand on it further in the next chapter. For now, block scoping occurs when you use let
or const
inside a block and confines the variable's scope to that block.
Variable scope is a crucial concept in programming. A solid grasp of scope in a given programming language is essential to fluency in that language. We explore it in great detail in our introductory courses at Launch School.
Thus far, all our function calls have used the functionName(obj)
syntax. We call a function by writing parentheses after its name and passing it zero or more arguments. If you want to convert a string to all uppercase letters, you might expect to use a function call like toUpperCase(string)
. However, you need to use a different syntax called method invocation.
Method invocation occurs when you prepend a variable name or value followed by a period (.
) to a function invocation, e.g., 'xyzzy'.toUpperCase()
. We call such functions methods. We cover this topic in more detail in the Core Curriculum. For now, think of the previous code as the method toUpperCase
returning a modified version of the string 'xyzzy'
.
It's unfortunate, but there is no easy way to determine whether you should use a function or method call for any given function. You must read the documentation or study the source code.
In JavaScript, and many other programming languages, we distinguish between two different ways to change things. We can either change what value is assigned (or bound) to a variable or we can change the value of the thing that is bound to the variable. The former is known as reassignment while the latter is known as mutation.
If you think of a variable as just a name that refers to a value at a specific place in memory, reassignment simply makes that name (variable) refer to a completely different place in memory and, hence, a different value. Mutation, on the other hand, changes the value that is actually stored in the memory that the name refers to. After mutating the value assigned to a variable, the variable continues to refer to the same place in memory.
This distinction is extremely important in any language that permits reassignment and mutation as it has significant impacts on the way those languages work.
One subtle but important distinction to make is the difference between reassigning a variable and reassigning an array element or an object property (both of which we'll discuss later). If a variable refers to an array or object, using that variable to reassign an element in the array or a property in the object does not reassign the variable. Instead, it mutates the array or object referenced by that variable. Here are a few examples illustrating the differences between reassignment and mutation in this more subtle code:
let num = 3; // A variable assigned to a primitive value
let arr = [1, 2, 3]; // A variable assigned to an array
let obj = { a: 1, b: 2 }; // A variable assigned to an object
num = 42; // Reassignment
arr[1] = 42; // Reassignment of array element, but NOT the variable
// The array referenced by arr is mutated!
obj.a = 42; // Reassignment of object property, but NOT the variable
// The object referenced by obj is mutated.
// You can still reassign the variables
arr = 42; // Reassignment; array is lost
obj = { b: 1, c: 2 } // Reassignment: now refers to a different object
We'll talk more about these concepts throughout this book and in almost every course at Launch School. For now, it's just important to understand what we mean when we talk about reassignment and mutation. Note that we may use some different terminology at times:
Reassignment is the assignment of a variable to a new value or object. We sometimes describe reassignment as changing the binding of a variable - that is, the value that is bound to the variable. We use binding and bound less often since the term "bind" is also used in a somewhat different manner.
Mutation is changing the value of the object or array referenced by a variable.
As much as possible, we use the terms mutation or mutate to describe any mutating operation. We're not as rigid when talking about reassignment, so you need to keep an eye out for those different ways to describe reassignment.
Sometimes a method permanently mutates the object that invokes the method: it mutates the caller. To contrast this with non-mutating methods, let's see an example:
let name = "Pete Hanson";
console.log(name.toUpperCase()); // => 'PETE HANSON'
console.log(name); // => 'Pete Hanson'
The toUpperCase
string method performs a non-mutating (non-destructive) operation. It preserves the previous value of the string: 'Pete Hanson
'. Non-mutating methods like toUpperCase()
often return a new value or object but leave the calling object unchanged.
Some methods mutate the object. We'll use an array method to illustrate. We haven't talked about arrays in detail yet, but we have enough knowledge for this example:
let oddNumbers = [1, 3, 5, 7, 9];
oddNumbers.pop();
console.log(oddNumbers); // => [1, 3, 5, 7]
The pop()
method removes the last element from an array but does so destructively: the change is permanent. Where the String toUpperCase
method returns a new value that's a changed version of the original string, pop
alters the array in place. In other words, it mutates its caller (the array).
We can also talk about whether functions mutate their arguments. Let's create a function that illustrates this concept:
function changeFirstElement(array) {
array[0] = 9;
}
let oneToFive = [1, 2, 3, 4, 5];
changeFirstElement(oneToFive);
console.log(oneToFive); // => [9, 2, 3, 4, 5]
This code uses the [index]
syntax to change the first element of the array that we pass to the changeFirstElement
function. When the function finishes running, we can see that the original array has changed.
Not all functions behave like that; none of our greeting functions alter the values we pass them. To see an example of a non-destructive array function, let's create a function that adds a new element to an array and returns the new array:
function addToArray(array) {
return array.concat(10);
}
let oneToFive = [1, 2, 3, 4, 5];
console.log(addToArray(oneToFive)); // => [1, 2, 3, 4, 5, 10]
console.log(oneToFive); // => [1, 2, 3, 4, 5]
The concat
method returns a new array that contains a copy of the original array combined with the additional elements that we supply with the arguments. Since concat
creates a copy of the original array and then mutates the copy, it leaves the original array intact, as shown on line #7.
One non-obvious point here is that mutation is a concern when dealing with arrays and objects but not with primitive values like numbers, strings, and booleans. Primitive values are immutable. That means their values never change; operations on immutable values always return new values. Operations on mutable values (arrays and objects) may or may not return a new value and may or may not mutate data.
How do you know which methods mutate the caller and which don't? It's helpful to understand that all primitive values are immutable, so this question never arises when dealing with them. However, there's no way to tell whether a function mutates an array or object. You have to use the documentation or memorize it through repetition.
We discuss object mutation in much more detail in our courses at Launch School.
If you have experience programming in other languages and wonder whether JavaScript is a pass-by-value or pass-by-reference language, JavaScript is both! It uses pass-by-value when dealing with primitive values and pass-by-reference with objects and arrays.
Let's look at some simple examples of combining functions. First, let's define add
and subtract
functions and call them:
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
let sum = add(20, 45);
console.log(sum); // => 65
let difference = subtract(80, 10);
console.log(difference); // => 70
Both add
and subtract
have two parameters and return the result of performing an arithmetic operation on, what we assume, are numeric values.
This works fine. However, in a process called function composition, JavaScript lets us use a function call as an argument to another function. Stated differently, we're saying that we can pass add(20, 45)
and subtract(80, 10)
as arguments to another function:
console.log(add(20, 45)); // => 65
console.log(subtract(80, 10)); // => 70
Passing the function call's return value to console.log
is the canonical example of function composition in JavaScript. It's useful but uninteresting. Life gets more interesting when you pass function call results to a function that does something more complicated:
function times(num1, num2) {
return num1 * num2;
}
console.log(times(add(20, 45), subtract(80, 10))); // => 4550
// 4550 == ((20 + 45) * (80 - 10))
Here, we pass the return values of add(20, 45)
and subtract(80, 10)
to the times
function, and we pass the return value of times
to console.log
! It produces the same result as the following verbose code:
let sum = add(20, 45);
let difference = subtract(80, 10);
let result = times(sum, difference);
console.log(result);
Let's see an even more complicated example:
add(subtract(80, 10), times(subtract(20, 6), add(30, 5))); // => 560
Let's break down what this code does:
add
: subtract(80, 10)
and times(subtract(20, 6), add(30, 5))
.
subtract
function call -- returns 70
.
times
function call, has two arguments: subtract(20, 6)
and add(30, 5)
.
subtract(20, 6)
returns 14
add(30, 5)
returns 35
times(14, 35)
times
call is 490
add(70, 490)
, which returns 560
.
We've seen that function calls always return a value, and we can pass that function call as an argument to another function call. Thus, it's vital to know what values our functions return. In the final analysis, those values get passed as arguments to other functions.
Thus far, we've studied one way to define functions in JavaScript:
function functionName(zeroOrMoreArguments...) {
// function body
}
In JavaScript, we call a function definition that looks like that a function declaration. A notable property of function declarations is that you can call the function before you declare it. We'll learn why that is in the Core Curriculum; all you need to know now is that you don't have to declare functions before calling them.
Let's see an example of invoking a function before declaring it:
greetPeople();
function greetPeople() {
console.log("Good Morning!");
}
Add the above code to a .js
file and run it with Node. You'll see that it works without raising any errors.
Let's see another way to define a function called a function expression.
let greetPeople = function () {
console.log("Good Morning!");
};
greetPeople();
That might look a little strange, but it's JavaScript that you'll see often. Most of it seems like a standard function declaration. However, since we're saving it to a variable, it's a function expression instead. Function expressions have one key difference from a function declaration: you cannot invoke a function expression before it appears in your program.
Our example declares a variable named greetPeople
and assigns it to the function expression after the =
sign. We can do that since JavaScript functions are first-class functions.
The critical feature of first-class functions is that you can treat them like any other value. In fact, all JavaScript functions are objects. Thus, you can assign them to variables, pass them as arguments to other functions, and return them from a function call.
The implications of functions being objects are far-reaching, though you won't really appreciate why that is until later in the Core Curriculum. We'll touch on this concept again in the Objects chapter, but we'll come back to it again in the Core curriculum.
The space after the word function
in a function expression is not required, so many programmers omit it:
let greetPeople = function() {
console.log("Good Morning!");
};
There's no one accepted style for whether the space should be included. Whether you do is up to your team's style guide or your personal preferences. At LaunchSchool, we don't enforce a style for the spacing. You'll see both styles in our material.
Any function definition that doesn't have the word function
at the very beginning of a statement is a function expression. Even wrapping what looks like a function declaration in parentheses creates a function expression:
(function greetPeople() { // This is a function expression, not a declaration
console.log("Good Morning!");
});
Another common example of function expressions occur in higher-order functions, a concept we won't explore in this book, but should show:
function makeGreeter(name) {
return function greeter() {
console.log(`Hello ${name}`);
};
}
Don't worry too much about what that code does. However, look closely at the definition of the greeter
function. That is not a function declaration -- it's a function expression.
There's a third kind of function in JavaScript called an arrow function. Syntactically, arrow functions look radically different from function declarations and expressions. Let's look at one:
let greetPeople = () => console.log("Good Morning!");
greetPeople();
Wow! That's quite a departure from the functions we've seen so far. Arrow functions are similar to function expressions, but they use a different syntax. The differences are not merely syntactic, however. We'll discuss them in more detail in the Core Curriculum.
For now, let's look at one interesting property of arrow functions: implicit returns. First, we'll convert the add
function from the previous section to use arrow function syntax:
let add = (a, b) => a + b;
That's much shorter! Note the lack of a return
statement. We can omit it in arrow functions when and only when the function body contains a single expression that is not itself surrounded by curly braces (the expression may have subexpressions, but the entire expression must evaluate to a single value). Suppose it contains two or more expressions or statements. In that case, you must explicitly return a value if you need it, and you must also use curly braces:
Run the following code in your browser's console; it won't work in Node! If you don't see a dialog that asks you to enter a number, move the console window out of the way.
let add = (a, b) => a + b;
let getNumber = (text) => {
let input = prompt(text);
return Number(input);
};
let number1 = getNumber("Enter a number: ");
let number2 = getNumber("Enter another number: ");
console.log(add(number1, number2));
On line #2, we define an arrow function that requires one parameter. The parentheses around the parameter name are optional in this case and are often omitted.
One important aspect of functions that all programmers need to understand is the concept of the call stack, or more casually, the stack.
The call stack helps JavaScript keep track of what function is executing as well as where execution should resume when the function returns. To do that, it works like a stack of books: if you have a stack of books, you can put a new book on the top or remove the topmost book from the stack. In much the same way, the call stack puts information about the current function on the top of the stack, then removes that information when the function returns.
Let's assume that we have the following code:
function first() {
console.log("first function");
}
function second() {
first();
console.log("second function");
}
second();
When this program starts running, the call stack initially has one item -- called a stack frame -- that represents the global (top-level) portion of the program. The initial stack frame is sometimes called the main
function. JavaScript uses this frame to keep track of what part of the main program it is currently working on.
Call Stack |
---|
- |
- |
- |
main |
When program execution reaches the function invocation on line 10, it first updates the main
stack frame with the current program location. JavaScript will use this location later to determine where execution should resume when second
finishes running.
After setting the location in the current stack frame, JavaScript creates a new stack frame for the second
function and places it on the top of the call stack: we say that the new frame is pushed onto the stack. Our call stack now looks like this:
Call Stack |
---|
- |
- |
second |
main: line 10 |
Note that the frame for the second
function is now stacked on top of the main
frame. While the second
frame is still on the stack, main
remains stuck beneath it, inaccessible. At the same time, the main
function becomes dormant and the second
function becomes active.
The second
function calls the first
function on line 6. That action causes JavaScript to update the second
frame so that JavaScript will know where to resume execution later. It then creates a new stack frame for the first
function and pushes it to the call stack.
Call Stack |
---|
- |
first |
second: line 6 |
main: line 10 |
Once the first
function begins executing, it invokes the console.log
method. All JavaScript functions and methods, including the built-in ones like console.log
, share the same call stack. Therefore, we need to record our current location and then push a new frame to the stack:
Call Stack |
---|
console.log |
first: line 2 |
second: line 6 |
main: line 10 |
Chances are, console.log
also has several internal function calls. However, we will ignore them and just assume that console.log
does all its work without any additional function calls. Instead, it just logs the message first function
to the console, then immediately returns.
When console.log
returns, JavaScript removes -- pops -- the top frame from the call stack. That's the frame for console.log
in this example. That leaves the previous stack frame exposed. JavaScript uses this frame to determine where execution should resume. In this case, execution resumes immediately after line 2.
Call Stack |
---|
- |
first: line 2 |
second: line 6 |
main: line 10 |
Eventually, the first
function will return. When that happens, the first
frame gets popped from the stack. That exposes the stack frame for second
, and that, in turn, tells JavaScript that it should resume execution on line 6.
Call Stack |
---|
- |
- |
second: line 6 |
main: line 10 |
Next, control passes to the console.log
call on line 7. Before console.log
is called, the stack frame for second
is adjusted to point to line 7:
Call Stack |
---|
- |
- |
second: line 7 |
main: line 10 |
We now invoke console.log
again. When that happens, it gets added to the stack:
Call Stack |
---|
- |
console.log |
second: line 7 |
main: line 10 |
When console.log
finishes, its stack frame gets popped from the stack, and control returns to second
:
Call Stack |
---|
- |
- |
second: line 7 |
main: line 10 |
The second
method then finishes executing, which causes its stack frame to be removed from the stack, exposing the stack frame for main
. The main
frame tells JavaScript to resume execution on line 10.
Call Stack |
---|
- |
- |
- |
main: line 10 |
Eventually, the main
function has no more code to run. When this happens, the main
frame gets popped from the stack, and the program ends.
Call Stack |
---|
- |
- |
- |
- |
The call stack has a limited size that varies based on the JavaScript implementation. That size is usually sufficient for more than 10000 stack entries. If the stack runs out of room, you will see a RangeError
exception together with a message that mentions the stack.
If you need a slightly more visual approach, take 10 minutes to watch this video.
Functions and methods are fundamental concepts in JavaScript programming. Knowing what a function does is crucial to your development as a JavaScript programmer. You'll use them all the time, in programs both big and small.
Understanding variable scope is essential towards becoming fluent with JavaScript. Function composition is something you'll use often, and understanding mutation helps you catch and avoid an entire class of bugs. There are multiple ways to declare functions: function declarations, function expressions, and arrow functions.
What does this code log to the console? Does executing the foo
function affect the output? Why or why not?
let bar = 1;
function foo() {
let bar = 2;
}
foo();
console.log(bar);
The code logs 1
to the console. foo
doesn't affect the value assigned to bar
on line 1 since JavaScript functions create an inner scope. Thus, the bar
variable on line 3 is not the same as the one on line 1. In the end, foo()
has no bearing on the final output.
Compare this result to the following code:
let bar = 1;
function foo() {
bar = 2;
}
foo();
console.log(bar);
In this case, console.log
would log 2 since bar
on line 1 is now in scope within the function. Thus, line 3 changes the global variable bar
to 2.
Video Walkthrough
In the exercises for the previous chapter, you wrote a dynamic greeter program named greeter.js
. Add a function to this program that solicits the user's first and last names in separate invocations; the function should return the appropriate name as a string. Use the return values to greet the user with their full name.
You need to pass a prompt argument to the function.
function getName(prompt) {
let readlineSync = require('readline-sync');
let name = readlineSync.question(prompt);
return name;
}
let firstName = getName('What is your first name? ');
let lastName = getName('What is your last name? ');
console.log(`Hello, ${firstName} ${lastName}!`);
Note how creating a function and giving it a meaningful name makes the intent of the program's main body a little clearer.
Video Walkthrough
Write a program that uses a multiply
function to multiply two numbers and returns the result. Ask the user to enter the two numbers, then output the numbers and result as a simple equation.
$ node multiply.js
Enter the first number: 3.141592653589793
Enter the second number: 2.718281828459045
3.141592653589793 * 2.718281828459045 = 8.539734222673566
function multiply(left, right) {
return left * right;
}
function getNumber(prompt) {
let readlineSync = require('readline-sync');
return parseFloat(readlineSync.question(prompt));
}
let left = getNumber('Enter the first number: ');
let right = getNumber('Enter the second number: ');
console.log(`${left} * ${right} = ${multiply(left, right)}`);
Video Walkthrough
What does the following code log to the console?
function scream(words) {
words = words + '!!!!';
return;
console.log(words);
}
scream('Yipeee');
It doesn't log anything. The return
on line 3 terminates the function before it can log anything.
Video Walkthrough
What does the following code log to the console?
function scream(words) {
return words + '!!!!';
}
scream('Yipeee');
This program also doesn't log anything. The function returns a value, Yipeee!!!!
, but it doesn't do anything with it. In particular, it doesn't write it to the console.
Video Walkthrough
In the code shown below, identify the following items:
function multiplyNumbers(num1, num2, num3) {
let result = num1 * num2 * num3;
return result;
}
let product = multiplyNumbers(2, 3, 4);
The function name is multiplyNumbers
.
The function arguments are the values 2
, 3
, and 4
between the parentheses on line 6. The function parameters are num1
, num2
, and num3
on line 1.
The function body consists of everything between the {
on line 1 and the }
on line 4. It doesn't matter whether you include or exclude the braces as part of your answer.
The function declaration is everything on lines 1-4. The function invocation is multiplyNumbers(2, 3, 4)
on line 6.
The function's return value is determined by multiplying the arguments together. That value is initially stored in the local variable result
in the function's body. It gets assigned to product
after the function returns.
This code's complete list of variables includes multiplyNumbers
, num1
, num2
, num3
, result
, and product
. In particular, the function name and its parameters are all variables. In this case, multiplyNumbers
is a global variable, but num1
, num2
, and num3
are local variables defined inside the function.
Video Walkthrough
Without running the following code, what do you think it will output?
function foo(bar, qux) {
console.log(bar);
console.log(qux);
}
foo('Hello');
The code will print:
Hello
undefined
The qux
variable inside foo
gets initialized to undefined
when we fail to provide an explicit argument in the invocation.
Video Walkthrough
Without running the following code, what do you think it will output?
function foo(bar, qux) {
console.log(bar);
console.log(qux);
}
foo(42, 3.1415, 2.718);
The code will print:
42
3.1415
The 3rd argument to foo
is ignored since the function definition only mentions two parameters.
Video Walkthrough
Identify all of the variables named on each line of the following code. You may assume that question
is the name of a built-in function in JavaScript (it is not, so this code won't work as written).
function multiply(left, right) {
let product = left * right;
return product;
}
function getNumber(prompt) {
return parseFloat(question(prompt));
}
let left = getNumber('Enter the first number: ');
let right = getNumber('Enter the second number: ');
console.log(`${left} * ${right} = ${multiply(left, right)}`);
multiply
, left
, right
: The function name and the parameter names are all variable names.
product
, left
, right
product
getNumber
, prompt
parseFloat
, question
, prompt
. This one is a little tricky. parseFloat
is an actual built-in function in JavaScript; we are pretending that question
also is a built-in function. As such, we know that both parseFloat
and question
are variable names.
left
, getNumber
right
, getNumber
console
, left
, right
, multiply
: console.log
is a little tricky here. console
is the variable name for the built-in Console object. In contrast, console.log
is the name of a method in that object. As such, log
is a property name, not a variable name.
Video Walkthrough
Using the code from Exercise 9, classify each variable name as either global or local. For our purposes here, you may assume that the code from Exercise 9 is the entire program.
multiply
, getNumber
, left
, right
, console
, parseFloat
, question
.
left
, right
, product
, prompt
Function names are global variables unless those functions are defined as an object property or nested inside another function. Thus, multiply
, getNumber
, console
, parseFloat
, and question
are all global variables in this program. In addition, function parameters and variables declared inside a function are always local variables. Thus, left
, right
, product
, and prompt
are all local variables.
In the next exercise, we'll see why left
and right
are both global and local variables.
Video Walkthrough
Using the code from Exercise 9, are the left
and right
variables on lines 1 and 2 the same as the left
and right
variables on lines 10-12? Explain your reasoning.
They are not the same variables.
The left
and right
variables declared on lines 10 and 11 are a little tricky. First, though they have the same names as the parameters defined for the multiply
function, they are not the same variables. (The function parameters shadow the variables on lines 10 and 11.) Furthermore, they are global variables even though they are only accessible on line 12 (technically, left
is also accessible on line 11 but is not used). They are considered global because they are defined at the topmost level of the program.
The left
and right
parameters on line 1 are local variables since function parameters are always local to the function. Thus, line 2 refers to the local variables.
Video Walkthrough