making a function within a loop is bad practice, plus it can cause some performance problems
There is some truth in these assumptions, but there is also a very big misunderstanding. You need to understand what happens when functions are created inside a loop, to be able to judge when to use it and when not to use it.
It is even necessary to create functions within the loop?
This is the first question you should ask yourself. It’s not that it’s bad practice or that it’s necessarily going to cause performance problems. First of all, it may simply be unnecessary.
Should the behavior of all its elements be exactly the same? It is possible to write a single function that serves to handle the event of all of them, without using anything that depends on the loop (the i
)? If it is, you don’t really need to create multiple functions. This would be a waste of resources, since each function created will occupy memory (this one of those "performance problems" that speak so much, and it may be irrelevant if the amount of elements receiving the click function is not too large).
There are two ways to use the same function for association of events such as click
to various elements:
1. Associate the function to each element individually
This is quite simple to do, you use a loop only for association, but the function is declared out of it:
// Função que alerta o ID da div clicada
function divClicada() {
// dentro de um event listener, this é o elemento
// associado ao listener
alert('A div #' + this.id + ' foi clicada!');
}
// Todas as divs da página
var divs = document.querySelectorAll('div');
// Associação de cada div à função
for(var i=0; i<divs.length; i++) {
divs[i].onclick = divClicada; // Sem parênteses!
}
But this can still make the page heavy if there are too many elements in the loop. Even if you create a single function, you still need to associate the elements with it one by one, so you can still suffer from the excess of associations, even these associations pointing to the same fate. There is a way to avoid this, which is preferable when dealing with many elements.
2. Delegation of events
In the DOM certain events propagate ("bubble") through the hierarchy of objects. Consider the following structure:
<div id="avo">
<div id="pai">
<div id="filho"></div>
</div>
</div>
A click on the element #filho
can be captured in any of the 3 Divs of the example, as the event goes up the tree until it reaches the top (until it reaches the element window
):
document.getElementById('avo').onclick = function(e) {
alert(e.target.id);
};
Demo
Look at the e
the function receives (no need to call e
, you can use any valid name). It is passed by the browser itself, and represents the event. The property target
represents the element that originated the event. With this it is then possible to listen to the click on several elements, creating a single association between an ancestor of it in the hierarchy and the function that treats the event.
This technique is called delegation of events, and is widely used when content is inserted dynamically on the page (for example via AJAX). This is because to associate a Istener to an element, it needs to exist at the time of association. Imagine a div with 3 others inside, each of the 3 with its own onclick
. If you exchange the contents of the div (for example, update a list of recent news), the original onclicks are lost. But if you have a single Listener of click
delegated to the outside element, that same function continues to serve. This is very useful and very practical.
Yes, it is necessary to create functions within the loop. And now?
Despite the many cases that can be avoided, sometimes it is necessary to create functions within a loop. However, this may not work as you expect. Consider the 3 inputs below:
<input value="valor original 1" />
<input value="valor original 2" />
<input value="valor original 3" />
I want to create a Library for the event blur
these inputs, so that when they lose focus, they restore their original values. There is how to do this with a single function, but consider that this option does not exist. A naive approach could be this:
var inputs = document.querySelectorAll('input');
for(var i=0; i<inputs.length; i++) {
inputs[i].onblur = function() {
this.value = inputs[i].value;
}
}
Demo
Only this code does not work. If you look at the browser console, you will see an error like this at the time of the Blur:
Uncaught Typeerror: Cannot read Property 'value' of Undefined
That means at the time of Blur inputs[i]
did not point to the input that just lost focus; instead it contained the value undefined
. This occurs as a matter of scope: all 3 functions are created in the same scope, and point to the same variable i
. The value of i
at the time of Blur (that is, after the end of the loop), it will always be 3
. And inputs[3]
does not exist, since in our example there are only 3 inputs (with indexes of 0
to 2
). Therefore, the value of inputs[i]
at that moment is undefined
.
If you read the contents of the last link, you should already know what the solution is for this: to introduce another function, called from within the loop and to capture the current value of i
every step. There are two ways to do this:
1. Creating a function that returns another
function criaListener(indiceDoElemento) {
return function() {
this.value = inputs[indiceDoElemento].value;
}
}
var inputs = document.querySelectorAll('input');
for(var i=0; i<inputs.length; i++) {
inputs[i].onblur = criaListener(i);
}
This is quite clean and readable. But there are times when we want to solve everything within the same loop.
2. Using a function immediately invoked (IIFE)
The code below works like the previous example, but creates and immediately executes a function within the same loop:
var inputs = document.querySelectorAll('input');
for(var i=0; i<inputs.length; i++) {
inputs[i].onblur = (function(indiceDoElemento) {
return function() {
this.value = inputs[indiceDoElemento].value;
}
}(i));
}
It is also possible to avoid function return, as seen in the second example of reply by Edgar Muniz Berlinck:
var inputs = document.querySelectorAll('input');
for(var i=0; i<inputs.length; i++) {
(function(indiceDoElemento) {
inputs[i].onblur = function() {
this.value = inputs[indiceDoElemento].value;
}
}(i));
}
On the link you passed you have a solution to your problem....
– MarceloBoni
but I would like to know of other alternatives to this link.
– user3632930
@user3632930, I believe you misunderstood, the problem is not using a loop to associate a function to click, but rather instantiating a function within a loop.
– Tobias Mesquita
@Tobymosque ah yes, so I saw instantiating a function within a loop can cause this function to be instantiated several times correct?
– user3632930
yes, that’s right, instantiating inside the for, you will have a different function for each element, if you instantiate outside the for, you will have the same function for all elements.
– Tobias Mesquita