This lesson plan goes over Javascript closures, which can at first seem like a confusing and complex concept to students. The good news is, they are very simple once you wrap your head around them.
This lesson plan contains three student activities accompanied by a review after each. Before each student activity, we will introduce the students to the topic with a short 5 minute demonstration.
-
Please be ready to open the
index.html
file enclosed in each demo and activity and open the developer tools for your given browser. This can often be accessed by right clicking anywhere on the page, choosing "inspect element" and then clicking on the "console" tab from there. -
Remember that closures can be a very difficult topic for students to understand at first. Students may feel frustrated and may even avoid asking questions because of this. Be sure to periodically ask the students how they are feeling about the concepts and revisit them if need be.
At the end of the session students should be able to:
-
Explain the concept of lexical scope
-
Implement closures in Javascript
-
Identify scoping issues within existing code
- Welcome the students to class and show them the following brain teaser to begin class
function logNumbers() {
for (var i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i)
}, 1000 * i)
}
}
logNumbers()
-
Explain that, by looking at this code, one might assume that it will log out
0, 1, 2
but it instead logs out3, 3, 3
. -
Explain that this is a popular interview question for its ability to test the applicantβs knowledge of closures and scoping.
-
Inform students that it is okay to not know exactly what closures are just yet because today we are taking a deep dive!
-
Reassure students that by the end of this lesson they will be able to recognize the issue with this code and explain why it behaves the way it does.
-
Explain that having a solid understanding of scoping and closures will not only help them during interviews, but make them better developers.
-
For this demo, open index.html in your browser and demo the following:
-
When we open the browser, we are greeted with a message that instructs us to open the developer console.
-
In the dev tools we see a
console.log
message. -
Open index.js and explain the following:
-
outerFunction
creates a local variable calledtopic
and another function calledsayTopic()
. -
sayTopic
is considered an inner function because it is declared inside theouterFunction
and only available in the body ofouterFunction
. -
Take note that the
sayTopic
function doesn't have any local variables of its own. Instead it can access any variable in the outer function's scope. -
This is an example of "lexical scoping", which describes how a parser resolves variables when dealing with nested functions.
function outerFunction() { let topic = 'closures' function sayTopic() { console.log(`Today we are covering ${topic}`) } return sayTopic }
- Notice that we assigned the return value of
outerFunction
to its own variable calledcaller
, and then we invoke it.
const caller = outerFunction() caller()
-
We are able to access the
topic
variable because of lexical scoping. -
The term lexical references the fact that location of a variable in code determines it's availability.
-
π‘ It is important to remember that nested functions always have access to their parent's function scope.
-
-
End the demo by asking the students the following:
-
π§βπ« Where can we learn more about lexical scope?
-
We can refer to the MDN documentation on Lexical Scoping
-
-
Point students in the direction of the correct folder for this activity, which can be found in the Unsolved folder.
-
Have the students work together using the directions found in the README.md file.
# Access Data from Lexical Scope
In this activity, you will work on this user story:
* As a developer, I want to understand lexical scoping by creating a function that accesses the surrounding state.
## Your Task
* It's done when I have opened `index.js` and modified the function so that it returns another function.
* It's done when the nested function logs out the `name` variable.
* It's done when I assign the return value of the outer function to another variable called `myFunction`.
* It's done when I have invoked `myFunction()` and ensured that the results appear in the developer console.
## Hints
* How can the definition of lexical scope help guide our understanding of this activity?
## Resources
* [Wikipedia Definition of Lexical Scope](https://en.wikipedia.org/wiki/Scope_(computer_science)#Lexical_scope)
- Remind students to use each other as a resource to "rubber duck" ideas off one another. Also, there is no shame in Googling!
-
βοΈ Ask students how they are feeling about lexical scope.
-
Advise students that we will go through the solutions together and if at any point they have questions, feel free to ask!
-
The following talking points can be expanded upon for review:
-
π¬ Lexical Scope
-
π¦ Closures
-
-
Navigate to index.js and demonstrate the following:
- This activity starts out with a function declaration called
names
, that declared a local variable calledperson
function names() { let person = 'Mac'
-
Inside our
names
function, we declared an inner function calledsayName
that console the a phrase containing theperson
variable. -
π‘ Notice that the
sayName
function is the return value for the outer function,names
. -
By declaring a function that accesses the variable
person
, we have created a closure. -
π‘ In Javascript all functions have access to the data in their parent function. This is an example of lexical scoping.
return function sayName() { console.log(`Hello! Nice to meet you ${person}`) }
-
In order to use the return value from the names function, we have two options:
-
We can add an extra set of parenthesis to the end of the
names
function call, or we can setnames
return value to a new variable. Let's go with the latter.
const myFunction = names()
- Finally, we invoke our newly created variable
myFunction
to see the result in the developer console!
myFunction()
- This activity starts out with a function declaration called
-
Open index.html in your browser and open developer console.
- Show the output in the developer console.
-
To end the review, ask students the following questions:
-
π§βπ« In your own words, what is the lexical scope?
-
Lexical scope describes how the Javascript interpreter resolves variable names within nested functions.
-
π§βπ« When in this activity did we create a closure?
-
The closure was created when we accessed a variable from the parent function's scope inside of the nested function's scope.
-
π§βπ« Be sure to check out the MDN Documentation on Closures. Don't stress if you are not clear on them yet, we will be discussing them more in the next demo.
-
-
For this demo, open index.html in your browser and demo the following:
-
When we open the browser, we are greeted with a message that instructs us to open the developer console.
-
In the dev tools, we see two
console.log
messages, both of which seem to be logging out some time in milliseconds.
-
-
Open index.js and explain the following:
-
Before we dive into the code, let's take a moment to refresh ourselves with what a closure actually is. It is easy to take them for granted in Javascript because we see them all over the place.
-
Closure is nothing more than the following:
nested function + outer context = closure
-
In other words, a closure is a small package containing the nested function and any variables it depends on from the parent scope.
-
In
index.js
we declared an outer function calledstopWatch()
that has a local variable ofstartTime
.
const stopWatch = () => { let startTime = Date.now()
-
Now we introduce our nested function called
getDelay()
that declares its own local variable,elapsed
. -
elapsed
will be used to log out the amount of milliseconds passed from the start to the current time.
const getDelay = () => { let elapsed = Date.now() - startTime console.log(`Milliseconds passed: ${elapsed}`) }
- The last thing we do in our outer function is return our nested function,
getDelay()
const stopWatch = () => { ... return getDelay }
- Just as we did in the previous activity, we set the return value of our outer function,
stopWatch
to its own variable calledtimer
.
const timer = stopWatch()
- Then we invoke
timer()
twice. The first is to simulate immediate execution, and the second is wrapped in asetTimeout
to induce some delay.
timer() setTimeout(() => { timer() }, 3000)
-
π‘ The Javascript engine keeps track of all of the variables that the nested function relies on, even if the parent function has finished running.
-
As a result, we are able to reference exactly the same start time, even though the stopwatch function has stopped running long before the timer was invoked.
-
We can see the results in the console:
Milliseconds passed: 0 Milliseconds passed: 3011
-
-
Ask the students:
-
π§βπ« Can you think of an example of closures in Javascript that you didn't know was a closure until now?
-
Since any function that accesses variables outside its local function scope, all functions that reference the global scope are closures.
-
-
To make for a smooth transition into the next activity, please ask students to navigate to the next activity folder in their IDE.
-
Make sure that students have navigated to the next activity found in 04-Stu_Closure-Basics
-
Have the students work together, using the directions found in the README.md file.
# Implement Closures
In this activity, you will work on this user story:
* As a developer, I want to demonstrate my knowledge of closures by creating a `makeCountDown` function that accesses the surrounding function state.
## Your Task
* It's done when I have created a function `makeCountDown` that returns a nested function.
* It's done when I have set the return value of `countDown` to a variable called `countingDown`.
* It's done when invoking the `countingDown` for the first time will start a countdown from `startingNum`.
* It's done when every subsequent invocation of `countDown` returns the `startingNum` subtracted from the `decreasedBy` variable.
## Notes
* Refer to the [MDN Documentation on Closures](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures#closure)
## Hints
* How can we use the comments inside `index.js` to guide us as to what our inner function should return?
* What kind of function is able to create other functions?
## Bonus
* Using [Google](https://google.com), what are some complications you might run into when using closures within for loops?
- Remind students to use each other as a resource to "rubber duck" ideas off one another. Also, there is no shame in Googling!
-
βοΈ Ask students how they are feeling about closures.
-
Advise students that we will go through the solutions together and if at any point they have questions, feel free to ask!
-
Remind students that if they didn't have enough time or were unsure of how to complete the activity, that is perfectly okay. Let them know we will go through the activity together.
-
The following talking points can be expanded upon for review:
-
βοΈ Parts of the closure
-
π Outer context (or surrounding state)
-
-
Open index.js in your IDE and demonstrate the following:
-
So far we have discussed local variables and accessing variables from the parent function. But what about variables that are passed as arguments to a function?
-
The goal of this activity is to create a function that counts down from the
startingNum
and decreases in increments of thedecreasedBy
number.
-
-
Open index.html in your browser and prepare to demonstrate the output in the developer console.
- When the function is invoked, it should remember the state of the number that it is decreasing. For example, if we started with
90
and decreased by5
, then the output should look something like this had we invoked the function 4 different times:
90 85 80 75
- To accomplish this, we need to make use of closures in Javascript.
- When the function is invoked, it should remember the state of the number that it is decreasing. For example, if we started with
-
Open index.js in your IDE and demonstrate the following:
- To start, we create a function called
makeCountDown
that accepts two arguments,startingNum
anddecreasedBy
:
const makeCountDown = (startingNum, decreaseBy) => { }
-
You will notice that our output will include the very number we are starting with. But how do we log out that number before we start decrementing?
-
In order to do this, we created a local variable called
countFrom
, which is essentially 5 more than the starting number.
let countFrom = startingNum + decreaseBy
-
Our inner function, or nested function, returns the result of subtracting
decreasedBy
, in our case5
, from thecountFrom
variable, which will evaluate to95
: -
Then we return the
countFrom
from the nested function.
let countFrom = startingNum + decreaseBy return (decrement = () => { countFrom -= decreaseBy return countFrom })
-
You can think of this function as a factory that creates other functions with their own respective outer context.
-
To demonstrate this, we created a variable called
countingDown5
with ourstartingNum
of90
and ourdecreaseBy
number of5
. -
We also did the same starting with
100
that decrements by2
. We set this to a variable calledcountingDown2
const countingDown5 = makeCountDown(90, 5) const countingDown2 = makeCountDown(100, 2)
- When this code is interpreted in the browser, you can see we have a total output of:
90 85 80 75 100 98 96 94
- This is all made possible by the way closures work. They take a snapshot of the outer context and remember them during execution time.
- To start, we create a function called
-
To end the review, ask students the following questions:
-
π§βπ« Can someone tell me how this function is different from the ones we have worked on in prior activities?
-
This function relies on local variables that get passed as arguments to the parent function rather than being declared locally.
-
π§βπ« What is the nested function and outer context that make up this closure?
-
The nested function is
decrement
, and because it reliescountFrom
from the parent scope, it creates a closure. Also, becausecountFrom
depends on thestartingNum
anddecreaseBy
variables, those too are part of the outer context. -
π§βπ« Be sure to check out the MDN Documentation on Closures and review some more examples if you're still unclear.
-
-
Ask students if they have any questions before moving on to the next demo
-
For this demo, open index.html in your browser and demo the following:
-
π§βπ« So far, we have been using somewhat abstract examples of closures. Now let's take a look at a practical application of closures with
fetch
! -
When we open the browser, we can see that there is an API request happening and the results are getting displayed to the DOM.
-
This function creates a closure that in turn makes different API requests based on a specific
topic
. In this case, it is searching an API for a random food. -
By using closures, we could easily create a new instance of this request and pass in a different topic such as
"app"
instead of"food"
.
-
-
Open index.js in the IDE and explain the following:
-
This activity involves some creation of DOM elements, which we will go over briefly.
-
First, we create some variables to select different elements on the page. One for
#results
and another for#results_container
const resultsEl = document.getElementById('results') const header = document.getElementById('result_container')
-
Next, we declare our outer function
fetchData
which accepts atopic
and returns a nested function,newSearch
. -
π‘ Our nested function depends on the
topic
variable declared in its parent. -
We set the
type
argument to have a default value in case none were passed to the function.
const fetchData = (type = 'food') => { return (newSearch = () => fetch(`https://random-data-api.com/api/${type}/random_${type}`) .then(data => data.json()) .then(response => { }) .catch(err => console.error(err))) }
-
Inside our nested function where we return a fetch request, we take the data that is returned and first convert it to JSON.
-
After that, we create some new DOM elements like
newResultsDiv
and a header to display our results on the screen:
return (newSearch = () => fetch(`https://random-data-api.com/api/${type}/random_${type}`) .then(data => data.json()) .then(response => { let newResultsDiv = document.createElement('span') newResultsDiv.innerHTML = `<h4>JSON results for random ${type}</h4>` header.prepend(newResultsDiv) resultsEl.innerText = JSON.stringify(response) }) .catch(err => console.error(err))) }
-
Keep in mind, the real magic is happening when we create specific instances of our
fetchData
function that will allow us to search for different stuff. -
π‘ Each one of these variables passes a different
type
to ourfetchData
function. The nested function depends on that type of variable, thus creating our closure with a different outer context for each one.
let searchFood = fetchData() let searchApp = fetchData('app') let searchName = fetchData('name')
- Finally we invoke our
searchFood
variable, allowing us to see the results in the browser.
searchFood()
-
-
Ask the students:
-
π§βπ« Can you think of another practical use for closures in Javascript?
-
Closures could be used to create private methods inside of a function similar to other programming languages.
-
-
Make sure that students have navigated to the next activity found in 06-Stu_Closure-Fetch
-
Have the students work together using the directions found in the README.md file.
# Search Always Displays Pictures of Dogs
Work toward resolving the following issue:
* As a user, I should be able to search for a term and see images related to my search term.
## Expected Behavior
When a user enters a search term in the search field and clicks submit, the app should display images related to the current search term.
## Actual Behavior
When a user enters a search term and clicks the submit button, nothing happens.
## Steps to Reproduce:
1. Open `index.html` in the browser
2. When the page loads, enter a search term of "computer" and click submit.
3. Notice that all the images are related to dogs.
## Hints
* How can we use our understanding of lexical scope to help us diagnose the issue with our `searchTerm`?
* Use the `console.dir` statements in the developer console to examine the function scope and the variables inside of it.
- Remind students to use each other as a resource to "rubber duck" ideas off one another. Also, there is no shame in Googling!
-
βοΈ Ask students how they are feeling about using fetch and closures.
-
Advise students that we will go through the solutions together and if at any point they have questions, feel free to ask!
-
The following talking points can be expanded upon for review:
-
π₯ Fetch API
-
π
document.getElementById
-
-
Navigate to index.html and demonstrate the following:
-
For this activity, we had to diagnose and fix an issue that was causing our search image field to not work properly
-
As you can see in the browser nothing happens when clicking submit, and the
console.log
messages seem to indicate that our search term isn't being passed properly. -
This activity was a tricky one, but we will go through the code together and find the cause.
-
-
Open index.js in the IDE and explain the following:
-
Much like the last activity, we have a lot of DOM manipulation elements that we will cover briefly.
-
First, we create some variables for various elements on the page:
const searchForm = document.getElementById('searchForm') const searchField = document.getElementById('searchField') const submitBtn = document.getElementById('submitBtn') const imageDiv = document.getElementById('imageContainer')
-
Because we are using fetch, we abstracted out our
BASEURL
andAPIKEY
into their own variables that will be used to form a Giphy endpoint. -
β οΈ Inform students that one would never want to put their API key in a file that will be public. Instead, you would want to use something likedot-env
to manage your private keys. More on that later.
const BASEURL = 'https://api.giphy.com/v1/gifs/search?q=' const APIKEY = '&api_key=dc6zaTOxFJmzC&limit=20'
-
We created a parent function called
search
that accepts aquery
parameter and returns a function callednewSearch
. -
Template strings are used to form the URL that our fetch request will hit.
const search = query => { console.log(`the search term is ${query}`) return (newSearch = () => fetch(`${BASEURL}${query}${APIKEY}&rating=pg`)) }
-
As we saw in the browser, the way this app is supposed to work is by entering a term into the search field and clicking submit.
-
With all forms, we need event listeners to tell the Javascript engine what to do when the user submits something.
-
Inside this function, we call our
search
function passing in what appears to be a variablesearchTerm
searchForm.addEventListener('submit', e => { ... search(searchTerm)() .then(result => result.json()) .then(images => { return displayImages(images.data) }) })
- You may have noticed in the starting code,
searchTerm
was initially declared as a global variable.
var searchTerm = searchField.value
-
π This means that at the time the Javascript interpreter parsed this file, the search term had an empty value. Here lies the source of our issue.
-
In order to resolve this issue, we need to capture the value of the input field when the user clicks the submit button. Let's make
searchTerm
a local variable of our event listener.
searchForm.addEventListener('submit', e => { ... let searchTerm = searchField.value ... })
-
Inside our event listener callback, we prevent the page from refreshing by calling
e.preventDefault()
and we also empty the content of theimageDiv
, so that image results don't compound one another. -
And now for the most important part - we invoke our
search
function. This time with a search term that is not empty. -
π‘ Notice that we can invoke a function's nested function by adding another set of curly braces after invoking it. Here is what our final event listener should look like:
searchForm.addEventListener('submit', e => { e.preventDefault() let searchTerm = searchField.value imageDiv.innerText = '' search(searchTerm)() .then(result => result.json()) .then(images => { return displayImages(images.data) }) })
-
You will notice that we are returning the result of
displayImage()
being invoked with the data from our API request. Then, we pass the image data as an argument. -
displayImage
is a helper function that addsli
elements for each image to the DOM.
const displayImages = images => { images.map( image => (imageDiv.innerHTML += `<li><img src=${image.images.downsized.url} key=${image.id}></li>`) ) }
-
π‘ The important thing to take away from this activity is that the closure created by
search
had an outer context that included an emptysearchTerm
variable, thus causing our bug. -
π§βπ« When dealing with closures, a very useful tool is
console.dir
. This allows you to take a look at the closure scope for a given function.
console.dir(search(searchTerm))
- Here is an example of what the preceding code would display in the console we had left our
searchTerm
in the global scope:
[[Scopes]]: Scopes[3] 0: Closure (search) query: "" 1: Script {searchForm: ... } 2: Global
- After moving the
searchTerm
inside our event listener, our closure was able to snapshot the correct value after searching for the term"Dog"
:
[[Scopes]]: Scopes[3] 0: Closure (search) query: "Dog" 1: Script {searchForm: ... } 2: Global
-
-
To end the review, ask students the following questions:
-
π§βπ« What is a closure and how can they be useful in reaching your career goals?
-
Closures are a very common interview topic, and can also help us become better developers by understanding how the Javascript interpreter works.
-
What can we do if we don't completely get this?
-
Be sure to review the MDN Documentation on Fetch and also the MDN Documentation on Closures
-
Thank the students for taking part in your class today, and answer any remaining questions regarding the topics covered in class.