- Define a function using a function declaration
- Define the term hoisting
- Define a function using a function expression
- Define the term "anonymous function"
- Define an IIFE: Instantly-Invoked Function Expression
- Define the term "function-level scope"
- Define the term "scope chain"
- Define the term "closure"
This lab provides a summation of the basics of JavaScript functions. Most of these ideas should feel familiar. Be sure to take time to experiment or read up on a concept if you're not comfortable with the idea before moving on. If you're struggling here, the remainder of this module will be challenging. Fix any gaps now before moving on.
We also recommend that you resolve the lab as you read through the sections. Reinforcing what you read with what you know how to type will make sure the concepts are locked in. We'll prompt you when it's a good time to shift modes from "reading along" to coding.
In JavaScript, the most common way to define functions is with a function declaration:
function razzle() {
console.log("You've been razzled!");
}
The word razzle
becomes a pointer to some stored, potential,
not-yet-actually-run bit of work (the function). We use the pointer to call
or invoke the function. We call the function by adding ()
after the pointer.
function razzle() {
console.log("You've been razzled!");
}
razzle()
//=> "You've been razzled!"
Interestingly, you can write function declarations after you call them:
razzle() //=> "You've been razzled!"
function razzle() {
console.log("You've been razzled!");
}
Functions can be passed arguments, given default arguments, etc. Here's a brief code synopsis:
function razzle(lawyer="Billy", target="'em") {
console.log(`${lawyer} razzle-dazzles ${target}!`);
}
razzle() //=> Billy razzle-dazzles 'em!
razzle("Methuselah", "T'challah") //=> Methuselah razzle-dazzles T'challah!
LAB: Implement a function called saturdayFun
. It should return a String
like
"This Saturday, I want to ....!"
Fill in the ...
with the activity that's
passed in as the first parameter. If nothing is passed in, default to
"roller-skate"
. Use the learn
program to verify you've gotten a working
implementation. Come back here once you've gotten just this set of tests passing.
JavaScript's ability to call functions before they appear in the code is called hoisting. For hoisting to work, the function must be defined as a function declaration.
Early in your programming career, you probably learned that programming languages feature expressions: arrangements of constants, variables, and symbols that, when interpreted by the language, produce a value.
1 + 1 //=> 2
"Razzle " + "dazzle!" //=> "Razzle dazzle!"
By analogy, if we agree that JavaScript has function expressions that look like this:
function() {
console.log("Yet more razzling")
}
what is in the variable fn
at the end of this block of code?
let fn = function() {
console.log("Yet more razzling")
}
This function expression evaluates to "stored work." Instead of the value 2
when 1 + 1
is evaluated, in fn
JavaScript stores work — something that's
much harder to visualize than 2
! If we ask JavaScript what's in fn
, it says:
let fn = function() {
console.log("Yet more razzling")
} //=> undefined
fn //=> ƒ () { console.log("Yet more razzling") }
Here, Chrome tells us that fn
points to an un-executed function, a "frozen"
function, a "potential" function, a "recipe" for work. The Chrome console symbolizes
this "frozen work" by putting the function definition after this cool special ƒ
character.
Here, fn
is a pointer to the stored block of work. But the work is merely
potential, it hasn't been done yet. Just as with function declaration,
we need to invoke or call the function.
To do the work, to make the potential real, to "un-freeze" the function, we
add ()
to the end, optionally adding arguments.
let fn = function() {
console.log("Yet more razzling")
} //=> undefined
fn //=> ƒ () { console.log("Yet more razzling") }
fn() //=> "Yet more razzling"
In any case, a function expression comes about when we write
function(){...}
and assign it to a variable. Very importantly, function
expressions are not hoisted. Since we assign these expressions to variables,
we'd expect things to operate in the same way they do when we assign a String
to a variable or the result of an arithmetic expression to a variable. Those
assignments are not hoisted, thus neither is a function expression.
LAB: Implement a function called mondayWork
. It should return a String
like
"This Monday, I will ... ."
Fill in the ...
with the activity that's
passed in as the first parameter. If nothing is passed in, default to
"go to the office"
. Use the learn
program to verify you've gotten a working
implementation. Come back here once you've gotten just this set of tests passing.
We also call the expression that produces a function that uses the template
function(){....}
an "anonymous function." In the previous example the
following was the anonymous function:
function() {
console.log("Yet more razzling")
}
Unlike a function declaration, there's no function name in front of the
()
. Since anonymous means, "without a name," this function is called,
sensibly enough, an anonymous function.
As a thought experiment, what happens here:
(function(baseNumber){ return baseNumber + 3 })(2) //=> ???
We recognize the first ()
as being those that we might use from arithmetic to
make something happen first in terms of order-of-operations. In the example
below, although we would normally expect *
to happen before -
, instead it
happens after since the ()
evaluation is performed earlier:
( 3 - 4 ) * 2 // => -2
So in the IIFE statement, the first parentheses return an anonymous function. It's like we assigned an anonymous function to a variable name, but didn't do the actual assignment and are instead left the right hand side of the assignment: a thing that can be invoked (instantly).
The second ()
are the ()
of function invocation like sayHello()
having
()
after the o
. It means, do the work in the function identified by the
name sayHello
.
Put these two components together and we're "invoking" the function immediately after defining it. Thus the name Instantly-Invoked Function Expression (IIFE for short).
(function(baseNumber){ return baseNumber + 3 })(2) //=> 5
Interestingly, any variables, functions, Array
s, etc. that are defined
inside of the function expression's body can't be seen outside of the
IIFE. It's like opening up a micro-dimension, a bubble-universe, doing all the
work you could ever want to do there, and then closing the space-time rift.
IIFEs are definitely science fiction or comic book stuff recalling the plot
of "Donnie Darko" or Dr. Strange's "mirror dimension."
We'll see some of the practical power of "hiding things" in IIFEs a little later in this lesson.
(ADVANCED) SYNTAX QUESTION Some keen-eyed readers might ask, "Why add parentheses around the function expression?" Why not:
function(x){ return x + 2 }(2) //=> ERRORinstead of:
(function(x){ return x + 2 })(2) //=> 4The reason is that JavaScript gets confused by all those bits of special symbols and operators sitting right next to each other. Just as we find the way ancient Romans wrote (all-caps, no spaces) VERYHARDTOREADANDHARDTOKEEPTRACKOF, JavaScript needs those "extra" parentheses to tell what's part of the function expression and what's part of the invocation. It shouldn't be necessary, but is.
JavaScript exhibits "Function-level" scope. This means that if a function is defined inside another function, the inner function has access to all the parameters (variables passed in) of, as well as any variables defined in, the outer function. This moves backward recursively too. Each of the enclosing parents' scopes are made available via the scope chain. Let's see things working before we define scope chain.
ASIDE: This is where people really start to get awed by JavaScript.
Consider this code:
function outer(greeting, msg="It's a fine day to learn") { // 2
let innerFunction = function(name, lang="Python") { // 3
return `${greeting}, ${name}! ${msg} ${lang}` // 4
}
return innerFunction("student", "JavaScript") // 5
}
outer("Hello") // 1
//=> "Hello, student! It's a fine day to learn JavaScript"
- We call
outer
, passing"Hello"
as an argument - The argument (
"Hello"
) is saved inouter
'sgreeting
parameter. The other parameter,msg
is set to a default value - Here's our old friend the function expression. It expects two arguments
which it stores in the parameters
name
andlang
withlang
being set as a default to"Python"
. This expression is saved in the local variableinnerFunction
- Inside
innerFunction
we make use of both the parametersname
andlang
as well as the parameters of innerFunction's containing (parent) function.innerFunction
gets access to those variables - Inside
outer
, we invokeinnerFunction
This might look a little bit weird, but it generally makes sense to our intuition about scopes: inner things can see their parent outer things. But with a simple change, something miraculous can happen:
function outer(greeting, msg="It's a fine day to learn") { // 2
let innerFunction = function(name, lang="Python") { // 3
return `${greeting}, ${name}! ${msg} ${lang}` // 4
}
return innerFunction
}
outer("Hello")("student", "JavaScript") // 1, 5
//=> "Hello, student! It's a fine day to learn JavaScript"
Amazingly, this code works the exact same. Even if the inner function
innerFunction
is invoked outside the parent function, it still has access
to those parent function's variables. It's like a little wormhole in space-time
to the outer
's scope!
Let's tighten this code up once more: instead of assigning the function
expression to innerFunction
, let's just return the function expression.
function outer(greeting, msg="It's a fine day to learn") {
return function(name, lang="Python") {
return `${greeting}, ${name}! ${msg} ${lang}`
}
}
outer("Hello")("student", "JavaScript")
//=> "Hello, student! It's a fine day to learn JavaScript"
Our "inner" function is now a returned anonymous function. To repeat: When
it came into existence, it inherited the values in outer
's parameters
greeting
and msg
. When we invoked outer
we provided the arguments for
greeting
and left msg
as the default. outer
then returned an anonymous
function that had its uses of greeting
and msg
set. It was almost as if
outer
returned:
return function(name, lang="Python") { // The "inner" function
return `Hello, ${name}! It's a fine day to learn ${lang}`
}
We invoked this returned or "inner" function" by adding ()
and passing the
arguments "student"
and "JavaScript"
which were then loaded into name
and
lang
inside the inner function. This filled in the final two values inside of
the template string and effectively returned:
return `Hello, student! It's a fine day to learn JavaScript`
//=> "Hello, student! It's a fine day to learn JavaScript"
Keep in mind, it's not the case that we have to invoke functions like this:
outer("Hello")("student", "JavaScript")
We could:
let storedFunction = outer("Hello")
storedFunction("student", "JavaScript")
//=> "Hello, student! It's a fine day to learn JavaScript"
In the previous example, we could call the "inner" function, the anonymous function a "closure." It "encloses" the function-level scope of its parent. And, like a backpack, it can carry out the knowledge that it saw - even when you're out of the parent's scope.
Recall the IIFE discussion, since what's inside an IIFE can't be seen, if we wanted to let just tiny bits of information leak back out, we might want to pass that information back out, through a closure.
Note: We're also using destructuring assignment, don't forget your ES6 tools!
let [answer, theBase] = (
function(thingToAdd) {
let base = 3
return [
function() { return base + thingToAdd },
function() { return base }
]
}
)(2)
answer() //=> 5
console.log(`The base was ${theBase()}`)
// OUTPUT: The base was 3
The mechanism behind all the cool stuff we just saw is the scope chain which allows functions defined in functions to access all their parent scopes' variables. Here's a simple example:
function demoChain(name) {
let part1 = 'hi'
return function() {
let part2 = 'there'
return function() { // Innermost
console.log(`${part1.toUpperCase()} ${part2} ${name}`);
}
}
}
demoChain("Dr. Stephen Strange")()() //=> HI there Dr. Stephen Strange
Through the scope chain, the function labeled //Innermost
has access to
name
, part1
, and part2
when it is called and runs the console.log()
statement. That's awesome wormhole, space-time, magic!
LAB:
- Implement a function called
wrapAdjective
.- It should return a function
- This "inner" function should:
- take a single parameter that should default to
"special"
. Name it however you wish. - return a
String
of the form "You are ..." where...
should be the adjective this function received wrapped in visual flair
- take a single parameter that should default to
- This "inner" function should:
- It should take as parameter a
String
that will be used to create visual flair - You may call the parameter whatever you like, but its default value should
be
"*"
- Call example:
let encouragingPromptFunction = wrapAdjective("!!!")
- It should return a function
- Thus a total call should be:
wrapAdjective("%")("a dedicated programmer") //=> "You are %a dedicated programmer%!"
Use the learn
program to verify you've gotten a working
implementation. Come back here once you've gotten just this set of tests passing.
Whew! That's a lot of recap with a lot of mind-bending stuff. Let's make sure that we review some of our easier basics.
The remainder of the tests are not new material. They're here to make
sure you remember how to work with Object
s and Array
s full of functions.
Use the tests to guide you in feeling confident working with functions.
In this lesson, we've covered the basics of function declaration, invocation, and function scope. As a refresher on your skills, we've provided a simple lab to make sure that you're set for the new information coming up in the rest of this module.