Closures
Functions Retain Their Scope
We just looked at how function scope works and how a scope chain is created. Just to recap: when anidentifier(i.e., a variable) is used, the JavaScript engine will check the scope chain to retrieve the value for that identifier. The identifier might be found in the local scope (either in the function or block). If it's not found locally, then it might exist in an outer scope. It'll then keep checking the next outer scope followed by the next outer scope until it reaches the global scope (if necessary).
Identifier lookup and the scope chain are really powerful tools for a function to access identifiers in the code. In fact, this lets you do something really interesting: create a function now, package it up with some variables, and save it to run later. If you have five buttons on the screen, you could write five different click handler functions, or you could use the same code five times with different saved values.
Let's check out an example of a function retaining access to its scope. Consider theremember()
function below:
function remember(number) {
return function() {
return number;
}
}
const returnedFunction = remember(5);
console.log( returnedFunction() );
// 5
When the Javascript engine entersremember()
, it creates a new execution scope that points back to the prior execution scope. This new scope includes a reference to thenumber
parameter (an immutableNumber
with the value5
). When the engine reaches the inner function (afunction expression), it attaches a link to the current execution scope.
This process of a function retaining access to its scope is called aclosure. In this example, the inner function "closes over"number
. A closure can capture any number of parameters and variables that it needs. MDN defines a closure as:
"the combination of a function and the lexical environment within which that function was declared."
This definition might not make a lot of sense if you don't know what the words "lexical environment" mean. TheES5 specrefers to a lexical environment as:
"the association of Identifiers to specific variables and functions based upon the lexical nesting structure of ECMAScript code."
In this case, the "lexical environment" refers the code as it was written in the JavaScript file. As such, a closure is:
- The function itself, and
- The code (but more importantly, the scope chain of) where the function is declared
When a function is declared, it locks onto the scope chain. You might think this is pretty straightforward since we just looked at that in the previous section. What's really interesting about a function, though, is that it will_retain_this scope chain -- even if it is invoked in a location_other_than where it was declared. This is all due to the closure!
So looking back at the above example -- afterremember(5)
is executed and returned, how is the returned function still able to accessnumber
's value (i.e.,5
)? In this section, we'll investigate how closures allow us to_store a snapshot of state_at the time the function object is created. Let's jump in!
Creating a Closure
Every time a function is defined, closure is created for that function. Strictly speaking, then,_every_function has closure! This is because functions close over at least one other context along the scope chain: the global scope. However, the capabilities of closures really shine when working with a nested function (i.e., a function defined within another function).
Recall that a nested function has access to variables outside of it. From what we have learned about the scope chain, this includes the variables from the outer, enclosing function itself (i.e., the parent function)! These nested functions close over (i.e.,capture) variables that aren't passed in as arguments nor defined locally, otherwise known asfree variables.
As we saw with theremember()
function earlier, it is important to note that a function maintains a reference to its parent's scope. If the reference to the function is still accessible, the scope persists!
Closures and Scope
Closures and scope are so closely related that you may even be surprised you had been working with them all along! Let's revisit an example from the previous section:
const myName = 'Andrew';
function introduceMyself() {
const you = 'student';
function introduce() {
console.log(`Hello, ${you}, I'm ${myName}!`);
}
return introduce();
}
introduceMyself();
// 'Hello, student, I'm Andrew!'
To recap:myName
is a variable defined outside a function, hence it's a_global_variable in the global scope. In other words,myName
is available for all functions to use.
But let's look closely at the other variable:you
.you
is referenced byintroduce()
, even though it wasn't declared withinintroduce()
! This is possible because a nested function's scope includes variables declared in the scope where the function is nested (i.e., variables from its parent function's scope, where the function is defined).
As it turns out, theintroduce()
function and its lexical environment form aclosure. This way,introduce()
has access to not only the global variablemyName
, but also the variableyou
, which was declared in the scope of its parent function,introduceMyself()
.
Let's see closures in action!
Here's the code from the preceding video.
QUESTION 1 OF 3
What is true about closures? Select all that apply:
A closure is created for a function only if that function is executed.Scope and closures are largely unrelated topics.A function maintains a reference to its parent's scope.
If the reference to a parent function is still accessible, the scope persists.
SUBMIT
QUESTION 2 OF 3
What is the output whenresult(10);
is executed?
function outerFunction() {
let num1 = 5;
return function(num2) {
console.log(num1 + num2);
};
}
let result = outerFunction();
result(10);
// ???
5
10
15
undefined
null
NaN
SUBMIT: AfterouterFunction()
is returned, it may seem that all of its local variables would be allocated back to available memory. As it turns out, however, the nestedinnerFunction()
still has access to thenum1
variable!
Let's take a closer look:outerFunction()
returns a reference to the inner, nested function. The return value of this invocation is saved inresult
. When this function is called, it maintains access to its scope; that is, all the variables it was able to access back when it was originally defined. This includes thenum1
variable in its parent scope. The nested function_closes over_these variables, and those variables persist as long as the reference to the function itself exists.
Whenresult(10);
is executed, then, the function is still able to accessnum1
's value of5
. As a result,15
is logged to the console.
Here's the code from the preceding video.
Applications of Closures
To recap, we've seen two common and powerful applications of closures:
- Passing arguments implicitly.
- At function declaration, storing a snapshot of scope.
/*
Declare a function named `expandArray()` that:
* Takes no arguments
* Contains a single local variable, `myArray`, which points to [1, 1, 1]
* Returns an anonymous function that directly modifies `myArray` by
appending another `1` into it
* The returned function then returns the value of `myArray`
*/
function expandArray() {
let myArray = [1, 1, 1];
return function() {
myArray.push(1);
return myArray;
}
}
- RESET QUIZ
- TEST RUN
- SUBMIT ANSWER
Garbage Collection
JavaScript manages memory with automaticgarbage collection. This means that when data is no longer referable (i.e., there are no remaining references to that data available for executable code), it is "garbage collected" and will be destroyed at some later point in time. This frees up the resources (i.e., computer memory) that the data had once consumed, making those resources available for re-use.
Let's look at garbage collection in the context ofclosures. We know that the variables of a parent function are accessible to the nested, inner function. If the nested function captures and uses its parent's variables (or variables along the scope chain, such as its parent's parent's variables), those variables will stay in memory as long as the functions that utilize them can still be referenced.
As such, referenceable variables in JavaScript are_not_garbage collected! Let's quickly look back at themyCounter
function from the previous video:
function myCounter() {
let count = 0;
return function () {
count += 1;
return count;
};
}
The existence of the nested function keeps thecount
variable from being available for garbage collection, thereforecount
remains available for future access. After all, a given function (and its scope) does _not _end when the function is returned. Remember that functions in JavaScript retain access to the scope that they were created in!
Summary
A closure refers to the combination of a function and the lexical environment in which that function was declared. Every time a function is defined, closure is created for that function. This is especially powerful in situations where a function is defined within another function, allowing the nested function to access variables outside of it. Functions also keep a link to its parent's scope even if the _parent_h as returned. This prevents data in its parents from being garbage collected.
At this point, we've worked a lot with functions declarations and function expressions. Did you know that you can write functions that can be immediately invoked after they're defined? We'll check out these immediately-invoked function expressions(IIFE's, or iiffy's) in the next section!
Further Research
- Memory Management on MDN
- Closures on MDN
- Lexical Environments in the ES5 spec