Understand Javascript Closure in Relation to Event Listeners

Illustration by moi

So you’ve become aware of a monster in Javascript called closure. Quality guides on this topic are plenty, but we learners never have enough practical examples, dont’ we?. Hence, in this article, I will provide a real-world example of Javascript closure as applied to event listeners.

It is safe to say that no matter how much you have read about closure, the light bulb in your head won’t shine bright until you’ve encountered closure in the wild world thousands of times.

However, the problem with most instructions is that their examples are too academic, not very practical, and largely taken out of context.

Worse, it’s hard to recreate them on a code playground for your learning purpose.

For that reason, I’m going to share with you a demonstration of Javascript closure in relation to event handlers, or event listeners.

I will also include pictures and gifs of the before and after scenarios, complete with source codes on Code Pen.

In fact, the example here is a simplified version of a frustrating Javascript closure problem that I encountered lately.

It took me a full day to figure out the answer, and another day to understand why it didn’t work initially.

I know that I’m neither the first, nor the last victim to this closure beast, so consider this article my open learning note, and let’s dive in.

div class="service-name">
<h2 class="service">Sport Photography</h2>
<figure class="featured-image">
<img src="https://source.unsplash.com/400x200/?sport" alt="Sport Photography">
</figure>
</div>
<div class="service-name">
<h2 class="service">Street Photography</h2>
<figure class="featured-image">
<img src="https://source.unsplash.com/400x200/?street" alt="Street Photography">
</figure>
</div>

I have a simple html document with two <div> tags. Each div provides information about a photography service.

Inside each <div> tag, there is a <h2> tag which contains the service name, sport photography or street photography.

Following each <h2> tag is a <figure> tag, which then contains an <img> tag to give readers an idea of what my photography services look like.

This looks fine, but I’d like to add a delight factor to it. I want the images to be hidden by default, and appear only when someone mouses over the service name.

Below is what I want the web design to look like:

Final look

At first, I thought this would be a piece of cake.

var service = document.getElementsByClassName('service');
var imgList = document.getElementsByClassName('featured-image');
function addLink() {
for (var i = 0; i < imgList.length; i++) {
imgList[i].style.display = "none";
service[i].addEventListener("mouseover",function() {
service[i].nextElementSibling.style.display = "block";

});

service[i].addEventListener("mouseout",function() {
service[i].nextElementSibling.style.display = "none";
});
}
}();

What I did was to get all elements with .service class aka the H2 headers for Sport Photography and Street Photography.

Next, I got all elements with .featured-image class, or the figure container that stores the pictures.

Then I set the display style for the .featured-image elements as none, which can only be achieved with for loop.

Last, I set up two addEventListener methods for mouseover and mouseout.

Mouseovercontains a function that will change the display style of .featured-image class to block when someone hovers over the .service class.

When they hover off that class, however, the images will disappear, just like the beginning.

All this for loop and event listeners are nested inside a self-executed function called addLink().

But look at what happens?

Javascript closure 2
Javascript closure 2

The images are always hidden. They never show up when I mouse over the headings.

You have encountered Javascript closure. Just see it as a symbol of a rites of passage that every aspiring Javascript learner has to deal with in their learning journey.

Anyway, here is the reason why our codse above don’t work:

We bind the variable i in the two addEventListener methods to the global i outside. There is always a mismatch in the value of the i inside and i outside.

The mouseover or mouseout event is only executed when someone hovers on or off the targeted element.

Meanwhile, the loop statement is executed as soon as the web page is loaded. Since we are using one big self-executing function, the display property for the .featured-image class takes effect right away. It disregards whatever happens to the mouseove and mouseoutfunctions.

Therefore, the targeted element’s style, the .featured-image class in this case will always be set to none.

I don’t intend to write about all the nuts and bolts of closure in Javascript, but here’s the most understandable definition of closure that I’ve found:

A closure gives you access to an outer function’s scope from an inner function.

Eric Elliott

Generally, declaring functions in a loop is a bad idea, but for the sake of practice, we’ll try to find a way to get through this problem.

IFFE, or Immediately Invoke Function Expression is a solution to protect the scope of your function and the variables within it.

In our case, we want variable i inside the mouseover and mouseout functions to be copied from, but not bound to the global i.

In other words, global i changes, but inner i stays the same, because the for loop and the functions for event listeners are operating on two different scopes.

Here’s what an IFFE looks like:

(function(i) {
return function() {
service[i].nextElementSibling.style.display = "block";
}
}(i))

An IFFE is wrapped inside brackets, and that’s how you tell it apart from traditional functions.

It usually has no name, because once we’ve created it, we’ll call it only once, and have no intention of calling it again.

In order to call the IFFE, we add a pair of brackets at the end of the function, just before the semicolon. Inside the brackets is, of course, the parameters for our IFFE.

Our final codes will look like this:

var service = document.getElementsByClassName('service');
var imgList = document.getElementsByClassName('featured-image');

for (var i = 0; i < imgList.length; i++) {
imgList[i].style.display = "none";
service[i].addEventListener("mouseover",(function(i) {
return function() {
service[i].nextElementSibling.style.display = "block";
}
}(i)));
service[i].addEventListener("mouseout",(function(i) {
return function() {
service[i].nextElementSibling.style.display = "none";
}
}(i)));
}

The result:

Create Javascript closure with IFFE
Create Javascript closure with IFFE

If you are wondering why the images are different, that’s because I use the Unsplash source for embedding placeholder images, and I choose random options.

To be honest, I never fully understand the different between let and var statements until I encounter closure, a rites of passage for any Javascript learners.

The let statement lets you declare a block scope variable. Actually, this is a fancy way to refer to the action of making your local variable i unchained by the global i.

for (let i = 0; i < imgList.length; i++) {
imgList[i].style.display = "none";
service[i].addEventListener("mouseover",function() {
service[i].nextElementSibling.style.display = "block";
});

service[i].addEventListener("mouseleave", function() {
service[i].nextElementSibling.style.display = "none";
);
};

Tadah!

Create Javascript closure with let statement
Create Javascript closure with let statement

Javascript closure has always been a mythological concept to grasp, but I think it’s even more flabbergasting when we involve for loop and addEvenetListeners.

Do you have a more efficient way to to display image on hover? Or is there any juicy lesson about Javascript closure that you would like to share? Please share your insights in the comments.

A web developer who lives with poor internet and always thinks of a new way to improve your site speed by 0.1%. Love automation in any form.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store