- 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 "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.
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 a bit of work (the function). We use the pointer to call
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!
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." If we ask JavaScript what it is 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. In this example, fn
is
a pointer to the stored block of work inside of the function (just as we saw
with function declarations). 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 turn 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 the following lessons, we'll learn more complex ways of taking a "potential" function and activating it.
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.
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(x){ return x + 3 })(2) //=> ???
We recognize the first ()
as being those that we might use from arithmetic:
( 3 - 4 ) * 2 // => -2
``
So the first parentheses return the anonymous function, the potential to do
work. The second `()` are the `()` of function invocation. So we're "invoking"
the function immediately after defining it.
```js
(function(x){ return x + 3 })(2) //=> 5
This bit of code is called an IIFE or "Instantly Invoked Function Expression." This stands to reason since its a function expression that we run immediately. IIFEs are a good way to make sure that you're grasping the content of this lesson up to this point.
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.
(
function(thingToAdd) {
let base = 3
return base + thingToAdd
}
)(2) //=> 5
// console.log(base) //=> Uncaught ReferenceError: base is not defined
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 think, 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.
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) as well as any variables defined in the function. Also: 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")
// ... lots of other code
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
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.