Variables Can Be Functions
JavaScript is one of many programming languages with first-class functions, which means that functions can be treated like any other variable. You can assign a function to be the value of a variable, a function can be the return value of another function, and most importantly: you can pass a function as an argument into another function. This is the central idea behind callbacks and higher-order functions.It's not immediately obvious that functions in JavaScript are the same as other variables, since the traditional syntax for declaring a function can look a lot different from the syntax for declaring other variables.
Your average variable in JavaScript can be declared with the keyword
const
.
const foo = 42;
const bar = 'Hello, world!';
If this variable needs to be reassigned or redeclared at any point after declaration, then it should
be declared
with the keyword let
instead.
let foo = false;
foo = true;
Functions on the other hand are traditionally declared using the function
keyword.
There's no
assignment operator (=
) to be found.
function foo() {
return 'Hello, world!';
}
In the code snippet above, foo
is a variable just as it was in the prior examples. In
this case, foo
's value is
the function object itself. To see what we mean, let's first console.log
the value
of a normal
variable.
const foo = 42;
console.log(foo
); // -> 42
We've initialized a variable with the label foo
and assigned it a value of the number
42. When we
console.log(foo)
, we see that its value is 42, as expected.
Now, let's take our function example from before.
function foo() {
return 'Hello, world!';
}
If we console.log
the evaluated result of invoking this function, the output will be
straightforward:
console.log(foo()); // -> 'Hello, world!'
But instead of invoking this function like foo()
, let's just log the value of
foo
itself.
console.log(foo);
/*
logs the following:
function foo() {
return 'Hello, world!';
}
*/
When a function is declared with the function
keyword, it creates a variable with the
label you gave
that function. The value assigned to this label is the function object you declared. So in our
example, we've declared a function with the label foo
. When we console.log
the value of the variable
foo
, we see that this value is the function object foo
.
The exact output from this
console.log
may differ depending on your JavaScript runtime environment, but the
process is the
same. For example, your output may not have any line breaks.
console.log(foo); // -> function foo() { return 'Hello, world!'; }
Alternatively, your output may even look like this:
console.log(foo); // -> [Function: foo]
The outcome is the same: foo
is a variable with the value of the function object
that we wrote.
To see this object, we could invoke the toString()
function method on foo
and log the result.
console.log(foo.toString());
/*
logs the following:
'function foo() {
return 'Hello, world!';
}'
*/
With functions being nothing more than special objects, they don't just have methods like
toString()
; they have other properties as well. Much like an array's
array.length
, functions
have properties such as function.name
.
Let's look at foo's name property.
console.log(foo.name); // -> 'foo'
So, as each console.log
has demonstrated, variables in JavaScript can take many forms.
A variable
could be a number, a variable could be a boolean, or a variable could be a function, among other
possibilities. You can pass functions around from one part of your code to another just as easily as
you can with any other variable. Keeping this in mind, callbacks and higher-order functions are much
easier to reason about.
Function Syntax
With the myriad of functions you're going to encounter in this guide and beyond, it's important to recognize the different forms that a function can take. So far, the functions we've looked at have used function declaration syntax. Here are some examples of functions declared this way:function divideByTwo(number) {
return number / 2;
}
console.log(divideByTwo(4)); // -> 2
function concatWithS(string) {
return `${string}s`;
}
console.log(concatWithS('apple')); // -> 'apples'
function addNumbers(number1, number2) {
return number1 + number2;
}
console.log(addNumbers(1, 2)); // -> 3
The purpose of these functions and their return values are straightforward. We see from its function
definition that divideByTwo
has one parameter: number
. Inside its function
body, divideByTwo
uses
the division operator to return a value equivalent to number
divided by two. Similarly,
concatWithS
and addNumbers
use a template literal and the addition operator respectively in order
to return
their desired output values based off of the data passed in as input.
From these examples, we can derive a simple function declaration template for illustration purposes:
// function declaration
function name(parameter1, parameter2) {
return returnValue;
}
// function invocation
name(argument1, argument2);
All function declarations follow this format. First comes the function
keyword,
followed by the name
of the function. The function's name is immediately followed by a list of its input parameters. The
example in our template has two, but functions can be defined with zero, one, or any other number of
parameters. After this comes the opening curly brace of the function's body. Between that and the
closing curly brace, we can include a list of statements for our function to execute. In most cases,
our list of statements will end with a return statement: the return
keyword followed by
any value
that we wish to return. The return value will often be influenced by the value of the function's
inputs.
Besides defining a function with a traditional declaration, the bottom of our template shows us an invocation of that function. A function is invoked when its name is used and followed by parens, in any context other than the initial function definition. Within those parens, we include any arguments we wish to pass into our function as input. These arguments are imported as the value of the function's parameters, and then the statements inside the function's body are executed.
In our example,
argument1
will be imported as the value of parameter1
, and
argument2
will likewise be
matched to parameter2
. If these parameters were used inside the function body, then for
that
specific call those parameters would have the value of those arguments within that body. Once the
function reaches and evaluates its return statement, the code stops running and the return value
will be passed back to the initial context from which the function was called. So, name(argument1, argument2)
has an evaluated result of returnValue
.
Looking back at our functions
divideByTwo
, concatWithS
, and
addNumbers
as well as their invocations,
it's clear to see how the code runs through the lens of our function declaration template. However,
what if the functions looked like this instead?
const divideByTwo = function (number) {
return number / 2;
};
console.log(divideByTwo(4)); // -> 2
const concatWitihS = function (string) {
return `${string}s`;
};
console.log(concatWithS('apple')); // -> 'apples'
const addNumbers = function (number1, number2) {
return number1 + number2;
};
console.log(addNumbers(1, 2)); // -> 3
These are not function declarations, these are function expressions. More
specifically, in each of
these examples we have declared a constant and assigned it the value of an anonymous function
expression. It's important to distinguish between both sides of the assignment operator when it
comes to initializing variables with function expressions as values. Much like the 42 in
const foo = 42
, the right side of this assignment in the examples above is just a
value. Just like 42 by itself
is not declaring anything, the function expression by itself is not declaring anything either,
unlike a function declaration. That is why there is a declaration happening on the left, using the
const keyword. Like any other value, a function expression doesn't always have to be assigned to a
label using const
or let
. It can also be passed into a function as an
argument, or returned from a
function as a return value.
You may have also noticed the anonymity of these functions; there is no name following the
function
keyword. While function declarations do need a name, it's optional for function expressions. In the
event that no name is used but the functixon is assigned to a variable, then the name can be
inferred
from the variable's label. So in the above example, divideByTwo.name === 'divideByTwo'
,
despite the
function expression itself not having a name. However, functions used in function expressions need
not always be anonymous, so this is also valid code:
// named function expression assigned to constant
const divideByTwo = function divider(number) {
return number / 2;
};
console.log(divideByTwo.name) // -> divider
Let's use all of this knowledge to come up with a template for function expressions.
// anonymous function expression assigned to constant
const name = function (parameter1, parameter2) {
return returnValue;
};
// function invocation
name(argument1, argument2);
You will encounter function expressions, both anonymous and named, throughout this guide and beyond.
You can think of them as being values that are functions; much like 42 is a value that is a number,
and "Hello, world!" is a value that is a string. Like these other values, function expressions will
often be assigned to variables using let
or const
, and they can be passed
into other functions and
returned from other functions as well.
Besides function declarations and traditional function expressions, there is another type of syntax you'll commonly see when it comes to writing functions: arrow function expressions. This is the most commonly used syntax that you'll see in this guide:
const divideByTwo = (number) => number / 2;
console.log(divideByTwo(4)); // -> 2
const concatWithS = (string) => `${string}s`;
console.log(concatWithS('apple')); // -> 'apples'
const addNumbers = (number1, number2) => number1 + number2;
console.log(addNumbers(1, 2)); // -> 3
In each example above, we have declared a constant and assigned it the value of an arrow function
expression. There seems to be quite a lot missing at first when compared to a traditional function
expression: the function keyword, the curly braces, and the return
keyword are nowhere
to be found.
However, it's important to note that an arrow function is just a special kind of anonymous function
expression, with different syntax. The list of parameters is still there, and the return value
is being returned implicitly.
All of the example functions we've been using so far have only had one statement within their body, as opposed to a longer list. If your function is like those examples and contains just a single expression, then arrow functions allow you to forgo the curly braces for the body, and leave out the
return
keyword. The expression after the arrow will be returned automatically; this is
called
implicit return.
const foo = () => 'Hello, world!';
console.log(foo()); // -> 'Hello, world!'
// the string was returned implicitly
However, many functions will have multiple statements to execute before a return is reached. If
there is more than one statement within your function, then your arrow function will need curly
braces to contain those statements. Similarly, if your code contains something that is not an
expression that can be returned, such as an if
statement or a variable declaration,
then curly braces
are needed for that situation as well. The return will not be implicit if curly braces are present,
so the return
keyword must be used if you wish to return a value, just like any other
function with
curly braces.
// curly braces are needed for this function
const game = (score) => {
if (score >= 100) return 'You won!';
else if (score >= 50) return 'Almost there...';
else return 'Try again next time!';
};
It's also worth noting that when an arrow function has exactly one parameter, the parens around that
parameter can be omitted. For consistency, the code in this guide will always use parens for an
arrow function's parameters, but it's important to know that both of the following functions work
the
same way:
const affirmation = name => `Great work, ${name}!`;
const affirmation2 = (name) => `You've got this, ${name}!`;
With all of this in mind, let's look at a template for writing arrow function expressions. We'll
also include the templates for the previous syntax as well, so you can easily see the differences
and similarities.
// arrow function expression assigned to a constant
const name = (parameter1, parameter2) => returnValue;
// anonymous function expression assigned to constant
const name = function (parameter1, parameter2) {
return returnValue;
};
// function declaration
function name(parameter1, parameter2) {
return returnValue;
}
// function invocation
name(argument1, argument2);
As you can see, these functions differ in appearance, but they're all invoked the same way. There
are differences under the hood when it comes to functions written using these different methods; in
some situations one kind of function is preferable over another, and in other situations certain
kinds of functions should be avoided altogether. However, within the scope of this guide, all of the
aforementioned function syntax will appear interchangeably, so make sure you're comfortable
recognizing and writing function declarations, function expressions, and arrow function expressions.
With this knowledge, you'll always be able to analyze your code and know exactly what each line is
doing at any given time.
Callback Structure
Now that we're familiar with the nature of functions and the various syntax that we'll encounter, let's look at how this all fits into this guide's main focus: callbacks and higher-order functions. We know that a variable can be a function, and that variables can be passed into functions as arguments. So what happens when you pass a function into a different function as an argument? In order to see this in action, let's start with an example problem that calls for a higher-order function:Write a function calledThis challenge's prompt is asking us to implement a function that simulates running a callback three times. Using our example functions from earlier, passing inthrice
that accepts two arguments: a callback function, and a value. Yourthrice
function should invoke the callback three times, following this logic:
On the first invocation,thrice
should pass its input value into the callback as an argument.
On the second invocation of the callback,thrice
should pass in the result of the first invocation.
On the third invocation,thrice
should pass in the result of the second invocation.
Finally, after these three function calls,thrice
should return the result of the third invocation.
Example 1:
Input
callback = (number) => number / 2
value = 8
Output
thrice(callback, value) === 1
Example 2:
Input
callback = (string) => `${string}s`
value = 'cat'
Output
thrice(callback, value) === 'catsss'
divideByTwo
and the number 8 should give
us a result of 1, which is what happens if you divide 8 by 2 three times. Similarly, passing in
concatWithS
and the string 'cat' should give us 'catsss', since that's what happens when you add an
's' to 'cat' three times. Let's start by writing out our function body:
const thrice = (callback, value) => {
// TODO
};
We now have a function that accepts the parameters specified in the prompt. Next, let's implement
the core idea: repeating an action three times. The most straightforward method for this is a simple
for loop.
const thrice = (callback, value) => {
for (let i = 0; i < 3; i += 1) {
// TODO
}
};
Now, let's tackle the trickier part. On each iteration, we're going to invoke the callback. The
argument that's passed into this callback should first have a value equal to the one passed into
thrice
. Before our loop starts, let's initialize a variable with this value, called result
. Then,
within the loop, we'll pass that variable into the callback, and save the output of that invocation
to a new variable called newResult
.
const thrice = (callback, value) => {
let result = value;
for (let i = 0; i < 3; i += 1) {
const newResult = callback(result);
}
};
Now we have a loop that invokes the callback, passing in the result
variable each time. This works
on our first iteration, but we know that on the next iteration, the value of newResult
should be
passed into our callback. So, let's just give that value to our result
variable before moving to
that second iteration:
const thrice = (callback, value) => {
let result = value;
for (let i = 0; i < 3; i += 1) {
const newResult = callback(result);
result = newResult;
}
};
Now, in the second iteration of our loop, result
has the value of the output of the first callback
invocation. We pass this value into the callback, and save it to newResult
. We then use the value of
newResult
to overwrite result
once again, before moving to the third and final iteration.
On the last iteration,
result
has the value of the output of the second invocation, so we pass this
into the callback and save it to newResult
once again. One final time, we overwrite result
with the
value of newResult
, and then the loop concludes.
Now, outside of the loop,
result
is left with the value of the third invocation of the callback.
Referring back to our prompt, this is the value we're supposed to return. So, as a final step, let's
add that return statement and then run some test cases.
const thrice = (callback, value) => {
let result = value;
for (let i = 0; i < 3; i += 1) {
const newResult = callback(result);
result = newResult;
}
return result;
};
const divideByTwo = (number) => number / 2;
const concatWithS = (string) => `${string}s`;
console.log(thrice(divideByTwo, 8)); // -> 1
console.log(thrice(concatWithS, 'cat')); // -> catsss
We've now finished implementing our function. A higher-order function satisfies at least one of two
criteria: accepting a function as an argument, or returning a function as its return value. Like
most of the solutions in this guide, thrice
fits that first criteria.
Now that we have a higher-order function, let's take a closer look at callback functions by examining the test cases and the way
thrice
is invoked.
In the code snippet above, we have defined our previously-discussed
divideByTwo
and concatWithS
functions. On each invocation of thrice
, we pass one of those functions in as the first argument.
This means that within the context of those invocations, divideByTwo
and concatWithS
are callback
functions, meaning they have been passed into another function as an argument and then invoked
inside that function.
One thing that's important to note is that we invoke
thrice
, the callback we pass in is a function
object, not a function invocation. There are no parens after the callback's label, because we are
not invoking it; we want to pass in the function itself as opposed to the evaluated result of
invoking that function.
Let's walk through exactly how our code runs, using the first test case as an example. First,
thrice
is invoked. divideByTwo
is imported as the value of the parameter callback
, and 8 is imported as the
value of the parameter value
. We then initialize our result
variable with value
, and enter our for
loop. Within this for
loop, we invoke callback
. This means that we are invoking the function object
that was passed in, using result
as the argument.
This next part is the key to understanding higher-order functions: whenever a function is invoked, a new execution context is opened. Look at the line as a whole:
const newResult = callback(result);
Before we can actually assign a value to newResult
, we have to find the value to assign, by
evaluating callback(result)
. This means that JavaScript will pause execution of the current function
thrice, and begin executing callback(result)
, which we know to be divByTwo(8)
.
We can now refer to the definition of the currently executing function,
divByTwo
, where we see that
8 is imported as the value of the argument number
. We evaluate 8 divided by two, and this result of
4 is implicitly returned. When the return
keyword is reached in a function, it returns to the
previous execution context, in the exact site that the function was called. The value after the
return
keyword is brought back to that previous context, where it can then be used. So, divByTwo(8)
has returned 4 to the previous context, where it is used as the evaluated result of the invocation
of divByTwo(8)
. It is now equivalent to this:
const newResult = 8;
From there, this process is repeated; two more calls to divByTwo
are made, returning the values of 2
and 1 respectively. And each time, JavaScript pauses the execution of thrice
, opens a new execution
context to evaluate the function call divByTwo(result)
, and then returns that evaluated result to
the previous context's call site in thrice
.
This process of opening a new context happens every single time a function is invoked, whether that's within the global context of our test case invoking
thrice
, or within the local context of
thrice
invoking divByTwo
. As we look at more and more higher-order functions and callbacks, it helps
to be able to follow the thread of execution from one context to another.
One final note: function expressions will often be passed directly into a function as a callback argument, without even being saved to a variable first. For example, the following invocations of thrice would also work:
console.log(thrice((number) => number / 2, 8)); // -> 1
console.log(thrice(function(string) {
return `${string}s`
}, 'cat')); // -> catsss
In the first example above, we have passed an arrow function expression into thrice
. That's the
function object that will be imported as the value as the first parameter. The second example may be
a little bit less obvious since it spans multiple lines, but we've still just passed an anonymous
function expression into thrice
as the first argument. The entire function across those three lines
is thrice
's first argument, which is why it's followed by a comma separating it from the second
argument, 'cat'. Recognizing when a function has been passed in as a callback even if it spans
multiple lines is crucial, as this will happen often with higher-order functions.
This concludes the "First-class Functions" section, which is the basis of understanding all the following challenges in this guide. If you know that functions are just like any other value in JavaScript, if you can identify the syntax patterns that functions can take, and if you're able to effectively follow the thread of execution in a higher-order function like
thrice
, then you'll be
able to fully grasp every single challenge in the CSX Callbacks and Higher Order functions module.