I’m working on a code to get the index of a clicked element so it can add or remove a class to display or hide the information. For it I used for
for iteration. But I don’t understand why is there an (i) after the event handler. I’m kind a newbie to coding so I want to understand everything.
Here’s the JavaScript code:
for (let i = 0; i < questions.length; i++) {
questions[i].addEventListener(‘click’,((e) => {
return function() {
if (clic[e].classList.contains(‘q-answered)) {
clic[e].classList.replace(‘q-answered’, ‘q-answeredno’);
} else if (clic[e].classList.contains(‘q-answeredno’)) {
clic[e].classList.replace(‘q-answeredno’, ‘q-answered’);
}
}
})(i))
}
var
to iterate through your questions
Put simply, it's making it an immediately-invoked function expression (or IIFE for short) and passing in a parameter you normally wouldn't otherwise have access to.
When a click event handler callback expects a function with a single variable. When the event is handled, the function is invoked and the JS runtime provides a pointer event back to your function to do something with. That's all well and good, but here you want to know the offset of the clicked element in the array from information gleaned out of the scope of this callback.
So you instead change the callback shape. You pass in your own function and wrap it in parentheses. In JS, you pass in the arguments to the function in parentheses following the definition. Your example uses the lambda syntax, so you start with the function itself:
(e) => {return ...;}
But if this is all that were passed in here, you'd get a PointerEvent assigned to e as it matches the anticipated callback shape. So you instead need to wrap this in parentheses:
((e) => {return ...;})
Great, but you want this function to have a very particular value passed in when it executes, so you define the arguments at the end. You're using i
as the variable identifying the index of the offset element, so we'll pass that in here.
((e) => { return ...; })(i)
Now, this means that when your event handler function is invoked for the first element, it'll practically look like the following:
((e) => {return ...; })(0); //Zero for the first zero-based index
This precludes the event handler callback assigning its own value to your first variable and means that now the function will be invoked, you'll pass the 0 (or otherwise set index property value) to your e
argument and the remainder of the return statement will execute accordingly.
@t.niese brings up a great point in the comments that I originally missed about closures and why they're quite relevant here.
Put simply, in JavaScript, you can refer to variables that are defined within a function's scope, to variables of the calling function's (e.g. parent) scope or any variables on the global scope. A closure is any function that's able to keep these references to these variables regardless of whether the parent has already returned or not.
If I have something like the following:
function listFruits(fruits) {
var suffix = "s";
for (var a = 0; a < fruits.length; a++) {
console.log(`I like ${fruits[a]}${suffix}`);
}
}
listFruits(['grape', 'apple', 'orange']);
// >> "I like grapes"
// >> "I like apples"
// >> "I like oranges"
As you'd expect, despite assigning suffix
outside of the loop, I'm able to use it within the loop to make each fruit name plural. But I can do this using an inner function as well:
function listFruits(fruits) {
var suffix = "s";
function pluralizeName(name) {
return `${name}${suffix}`;
}
for (var a = 0; a < fruits.length; a++) {
var pluralName = pluralizeName(fruits[a]);
console.log(`I like ${pluralName}`);
}
}
listFruits(['grape', 'apple', 'orange']);
// >> "I like grapes"
// >> "I like apples"
// >> "I like oranges"
Again, despite suffix
being assigned in the parent of the pluralizeName
function, I'm still able to assign it in the return string to log to the console in my loop.
So, let's put a timeout callback in there and see what happens:
function listFruits(fruits) {
var suffix = "s";
for (var a = 0; a < fruits.length; a++) {
setTimeout(function() {
console.log(`I like ${fruits[a]}${suffix}`);
}, 1000);
}
}
listFruits(['grape', 'apple', 'orange']);
// >> "I like undefineds"
// >> "I like undefineds"
// >> "I like undefineds"
Why didn't we list the fruit we like here as before? a
is still being defined in the parent and it does attach the suffix
value as it should, so what gives?
Well, our loop starts at a = 0, sets the timeout to execute the callback in 1000ms, then increments a
to 1, sets the timeout to execute the callback in 1000ms, then increments a
to 2, sets the timeout to execute the callback in 1000ms, then increments a
to 3, sets the timeout to execute the callback in 1000ms, then increments a
to 4 at which point a
is greater than the number of fruits passed in (3) and the loop breaks out. But when the timeout callbacks run then, a
now has a value of 4 and fruits[4]
is undefined, so we see that in the console logs.
Ok, but we want to be able to reference the value of our iterating index locally so the first callback works against the first object, the second against the second and so on, so how do we make that available to the callback? We use the IIFE approach covered above.
function listFruits(fruits) {
var suffix = "s";
for (var a = 0; a < fruits.length; a++) {
(function() {
var current = a;
setTimeout( function() {
console.log(`I like ${fruits[current]}${suffix}`);
}, 1000);
})();
}
}
// >> "I like grapes"
// >> "I like apples"
// >> "I like oranges"
It works here because we create a new closure for each of the functions created by the loop. When the function is created, we take the current value of a
from the parent and assign it to a local variable so that when the setTimeout
method fires later, it uses that local current
value to properly access the intended index of the fruits
array.
But rather than capture the variable as I do above in var current = a;
, I can instead pass the a
variable into the IIFE as a parameter and it will have exactly the same effect:
function listFruits(fruits) {
var suffix = "s";
for (var a = 0; a < fruits.length; a++) {
(function(current) {
setTimeout( function() {
console.log(`I like ${fruits[current]}${suffix}`);
}, 1000);
})(a);
}
}
listFruits(['grape', 'apple', 'orange']);
// >> "I like grapes"
// >> "I like apples"
// >> "I like oranges"
Our IIFE populates the current
variable with the argument a
passed in making it available locally and we get the expected outcome.
let
, so how does that change anything?Prior to ES6, we had only the global and function scopes requiring the use of the IFFEs to introduce local function scopes as we saw above. With ES6, we got a new "block scope" which essentially scopes everything within two curly braces, including any number of child blocks within that, but only if the variable is assigned using the const
or let
keywords. var
still only assigns to a global or function scope.
Let's revisit the example above in which we received all the undefined values and replace our use of var
with let
.
function listFruits(fruits) {
var suffix = "s";
for (let a = 0; a < fruits.length; a++) { //Note the change to 'let' here
setTimeout(function() {
console.log(`I like ${fruits[a]}${suffix}`);
}, 1000);
}
}
listFruits(['grape', 'apple', 'orange']);
// >> "I like grapes"
// >> "I like apples"
// >> "I like oranges"
And this time it works because the value of a
persists as assigned to the setTimeout callback's block scope.
Let's bring it back full-circle then. If you were using var
, your sample scopes the present value of i
to the IIFE so each of your event handler callbacks can access the appropriate offset node.
But since you're using let
, the use of an IFFE certainly won't hurt anything, but it's unnecessary as the intended value of i
is available to your callback functions via their block scope. As such, you can simplify to remove the IIFE and suffer no consequences.
for (let i = 0; i < questions.length; i++) {
questions[i].addEventListener(‘click’, function() {
if (clic[i].classList.contains(‘q-answered)) {
clic[i].classList.replace(‘q-answered’, ‘q-answeredno’);
} else if (clic[i].classList.contains(‘q-answeredno’)) {
clic[i].classList.replace(‘q-answeredno’, ‘q-answered’);
}
});
}
Should you have questions about any of this, please leave a comment and I'd be happy to edit to address it.