Javascript Closure With Array of Nodes - javascript

I'm currently learning javascript from the following book 'JavaScript: The Good Parts - O'Reilly Media', which says the following:
It is important to understand that the inner function has access to the actual variables of the outer functions and not copies in order to avoid the following problem:
// BAD EXAMPLE
// Make a function that assigns event handler functions to an array of nodes the
wrong way.
// When you click on a node, an alert box is supposed to display the ordinal of the
node.
// But it always displays the number of nodes instead.
var add_the_handlers = function (nodes)
{
var i;
for (i = 0; i < nodes.length; i += 1)
{
nodes[i].onclick = function (e)
{
alert(i);
};
}
};
// END BAD EXAMPLE
Question: I don't understand what the problem is, if someone could give me a clear example with numbers and results that would greatly be appreciated.

The value of i changes every time the loop is incremented, and the event handlers will alert whatever the current value of i is. So if there are 8 elements, all of them will pop-up a value of 7 when the loop is done running.
The point of the example is that many people assume that when each handler is initially bound with the current value of i (e.g. 0, 1, 2, etc.) that it then doesn't change as i is incremented. This example demonstrates that this isn't the case, and that the event handler functions always have access to the current value of i, even after being bound.

The issue here is that the function you pass to onclick keeps a reference to the same i that is incremented at each loop turn. Thus, when the click is triggered, i has been incremented to match the number of elements.

Related

I'm having troubles understanding the logic when using for loops with eventlisteners in javascript

I am learning web development and right now i'm working on vanilla javascript. I'm following a class on Udemy and everything is going well but there is a challenge where we are supposed to build a drumkit so everytime we click on a button it should trigger a function. So the solution the instructor is giving is using a for loop
var nbButtons = document.querySelectorAll(".drum").length;
for(i = 0; i<nbButtons; i++){
document.querySelectorAll(".drum")[i].addEventListener("click", handleClick);
function handleClick() {
alert("I got clicked!" + i);
}
}
So if I try to analyze the code I understand than :
We create a variable to keep track of the number of .drum elements (querySelectorAll(".drum") returns an array of all .drum elements and we get it's length.
We start a loop : We start listening for clicks on the .drum element whose number is equal to i and start the function handleClick.
So I tried it and it works but I don't understand why. the loop starts when the page is loaded which means i = nbButtons very quickly (i added console.log in the loop and it does) so logically it should listen only on clicks on the last element ?
I know i'm missing something but I don't know what.
Thanks in advance
We start listening for clicks on the .drum element whose number is equal to i and start the function handleClick.
This is the part you misunderstood.
You don't immediately start listening to the current element. This line registers the listener which then listens more or less in the background. So in your for loop you register an event listener for each array index. The event listener isn't overwritten on the next iteration. You're able to register multiple event listeners at once, even on the same element.
You can verify that in devtools:
More information on what addEventListener() does: https://www.w3schools.com/js/js_htmldom_eventlistener.asp
nbButtons is just the number of buttons, say 5 buttons.
Then you use a for loop to go through all the 5 buttons. The loop executes 5 times, and this number is temporarily stored in the i variable. So, using i you can also get to each button and add the event listener to that button.
You can log this with
for(i = 0; i<nbButtons; i++){
console.log(i);
console.log(document.querySelectorAll(".drum")[i])
}
There are several parts of your code that I would write differently (some pointed out in other answers), but the key problem is that you have a global variable i, which will already have the value nbButtons by the time any of the handleClick functions actually run - so it's no surprise that this is the value you see logged on each click.
To make sure the correct value of i is logged for each button, you have to ensure the function you add as the event handler actually uses the "current" value. Here is one way:
var nbButtons = document.querySelectorAll(".drum").length;
for(let i = 0; i<nbButtons; i++){
document.querySelectorAll(".drum")[i].addEventListener("click", function() { handleClick(i);} );
}
function handleClick(i) {
alert("I got clicked!" + i);
}
There are a few key differences from your faulty code. For one, I've pulled the definition of the handleClick function out of the loop - this doesn't actually matter here (function declarations are scoped to functions, not arbitrary blocks like loops), but is neater and matches better how the code actually behaves. More importantly, I've made it take a parameter, i, and made sure the correct i is passed in in each addEventListener call.
And most importantly of all, I've replaced your global variable i in the loop with a local one, declared with let. This ensures that when the button is clicked and the function - function() { handleClick(i);} is called, it sees not a global i whose value was long ago incremented past the intended value, but one scoped to the particular loop iteration, which therefore only ever had the one value. Note that let is crucial to make this work, using var would not (without some extra complications anyway) - see this classic question for more detail
What you have done in your for loop is search for a ".drum" using the queryselectorall function which I do not think is efficient - if you must loop with "for";
I think you should:
var nbButtons = document.querySelectorAll(".drum");
var nbButtonsLen= nbButtons.length;
for(i = 0; i<nbButtonsLen; i++){
nbButtons[i].addEventListener("click", handleClick);
}
function handleClick() {
alert("I got clicked!" + i);
}

JS addEventListener arguments

I have an HTML page with several forms (created dynamically), so I get them in JS into an array, and then I add an EventListener to each one, like this:
var forms = document.getElementsByTagName('form');
for(i=0; i < forms.length; i++) {
forms[i].addEventListener('change', function(){
checkAllFilled(i);
});
}
So in function checkAllFilled I do some stuff.
The problem is that if I have 7 forms (from forms[0] to forms[6]) and I work over forms[2], it is always called checkAllFilled(7), instead of calling checkAllFilled(2). It's like the variable i was set in the last value over the for loop.
How would I do this?
Thanks!
It's like the variable "i" was set in the last value over the for
loop.
This is exactly what's happening.
Think about it. Your event handler runs later, after the for-loop has been and gone. At that time, the variable has whatever value the loop left it with - hence, the event will always report the same, single value.
Instead, you need to bind the iterative value in via a closure. There's a few ways. One is via an immediately-executed function (IEF).
forms[i].addEventListener('change', (function(i) { return function(){
checkAllFilled(i);
}; })(i));
We pass the iterative value to our IEF as a function variable. This way, it's passed to the handler via a closure. It's worth reading up on these.
Another way is just to use let from ECMAScript 2015.
for(let i=0; i < forms.length; i++) {
forms[i].addEventListener('change', function(){
checkAllFilled(i);
});
}
You are always calling the last generated event listener, since you generate first these listeners (all with same name) and then call the last one of them.
The best solution here is to assign names for the functions handling the events based on the counter value i.
Use forEach()instead of a for loop:
var forms = document.getElementsByTagName('form');
[].slice.call(forms).forEach(function(form, i) {
form.addEventListener('change', function(){
checkAllFilled(i);
});
});

If I use object.getElementsByTagName(tagName) in a for statement

If I use object.getElementsByTagName(tagName) in a for statement,
for (index = 0; index < object.getElementsByTagName(tagName).length; index++) {
object.getElementsByTagName(tagName)[index].property = value;
}
Does the browser instantiate a new nodeList object for every pass through the loop, or does the browser simply refer to a single generated list every time; or maybe, it instantiates a list, references the object specified and unloads the list object every pass through the loop?
I've been wondering if its better to store the nodeList object to a variable and reference it when neened.
This is way better code for the reasons listed below:
var elems = object.getElementsByTagName(tagName);
for (var index = 0, len = elems.length; index < len; index++) {
elems[index].property = value;
}
It cannot ever call getElementsByTagName() more than once. It only fetches the nodeList once.
It doesn't rely on any specific browser implementation in order to be efficient.
It preloads the length of the nodeList so even that isn't refetched each time through the loop.
It's just a safer way to make your code efficient.
Safer code makes the implementation questions you asked irrelevant.
Does the browser instantiate a new nodeList object for every pass through the loop or does the browser simply refer to a single generated list every time;
You can test this easily by testing whether two calls to getElementsByTagName return the same object:
document.getElementsByTagName('div') === document.getElementsByTagName('div')
// true
It seems that calling this method with the same argument does indeed return the same NodeList object (in Firefox and Chrome at least). It does not generate a new list every time.
So you might think that calling it in the loop over and over again won't make a difference. However, as I see it, there are multiple reasons why would want to store the list in a variable:
You have an unnecessary function call in each loop.
You don't know what actually happens behind the scenes. Even though the same object is returned, the might be procedures running which make sure that the list reflects the current state of the document, which would not happen if you didn't call the function.
Most importantly: The DOM specification doesn't seem to require that the same list is to be returned. That it does in Firefox and Chrome (where I tested it) might just be an implementation detail, so I wouldn't rely on this behavior. In fact, the specification explicitly states to return a new list:
Return Value: A new NodeList object containing all the matched Elements.
I've been wondering if its better to store the nodeList object to a variable and reference it when needed.
Yes it is. You don't even have to call getElementsByTagName again when elements are added or removed because the returned NodeList is live, i.e. any changes made to the document are directly reflected in the list, without you having it to update explicitly.
Certain operations, like accessing the length of the list will also trigger a reevaluation. So, additionally to storing the list in a variable, you might want to cache the length as well:
var nodes = object.getElementsByTagName(tagName);
for (var index = 0, l = nodes.length; index < l; index++) {
nodes[index].property = value;
}
This can be very handy or very confusing, depending on what you need. If you only want a list of nodes that exist at the moment the function is called, you have to convert the NodeList to an array:
var nodes = Array.prototype.slice.call(object.getElementsByTagName(tagName), 0);
this test shows that it first gets the array calculates the length each time!
var testObj = {
getArray: function () {
console.log( 'getting array' );
return this._array;
},
_array: ['a','b','c']
};
for ( var index = 0; index < testObj.getArray().length; index++ ){
document.write( testObj.getArray()[index] );
}
I would either store the elements first, and reference it's .length each time, or just store the length in a variable :)
Your browser is generating a new list each time.
But this code will work as index is changing every run.
It is a good practice to first save node list in some var:
var elements = object.getElementsByTagName(tagName);
for (index = 0; index < elements; index++) {
elements[index].property = value;
}
you can see your aproach as:
var index = 0;
while(index<object.getElementsByTagName(tagName))
{
elements[index].property = value;
index++;
}
What did you expect it to do? Read your mind? Did you expect the JS processor to have the semantic information that object.getElementsByTagName(tagName) will (probably) return the same value each time it is called, and is thus "loop-invariant" and can thus be hoisted out of the loop (see http://en.wikipedia.org/wiki/Loop-invariant_code_motion)?
Perhaps you imagined that the browser is somehow "caching" the list of all elements with a particular tagname under the element, and then just returning the same list in subsequent calls? Although it's hard to tell what a browser is doing inside without looking at the code, it may not be, or one browser might be and another browser not. Even if the browser does cache the result, granted that any performance hit is likely to be minor in the overall scheme of things, calling the API again and again will always be much less efficient that referring to a variable that's already set. And it is not impossible that what you do with each element of the nodelist, such as setting a property, could cause the cache to be reset.
As a minor point, assuming the list is created over and over again, it is not then "unloaded"; there is no such concept in JS. In technical terms, its status would be "just left sitting there". It will be garbage collected soon enough.
So yes, by all means call getElementsByTagName just once. Actually, you can call it once at the beginning of your program (assuming there's just one object and tagname you're interested in), since it returns a "live" list. See https://developer.mozilla.org/en-US/docs/Web/API/element.getElementsByTagName.

How to pass a 'constant' parameter in jquery

I have the following attached to a div element, which detects when a user right clicks on a div and calls the function theFunction.
$('#box_'+i).bind('mousedown',function(e){if(e.which==3)theFunction(e,i)});
The code is run inside a loop (where the i variable that is incremented), as there are multiple divs that need right click functionality on the page. However when theFunction is called it always passes the last value of i instead of the value that it was when the bind() was done. Is there a way to make it constant so it wont change i for the previous divs as the loop is run?
You'll have to capture the value of i in a local scope, inside of your loop:
// Start your loop
(function(i){
$('#box_'+i).bind('mousedown', function(e){
if (e.which==3) theFunction(e,i)
});
})(i);
// end your loop
The closure solutions are all good (they work just fine), but I thought I'd point out another way to do it. You can also access the object that triggered the event and pull the number off the id of the object. That would work something like this:
$('#box_'+i).bind('mousedown',function(e){
var matches = this.id.match(/_(\d+)$/);
var num = parseInt(matches[1], 10);
if(e.which == 3) theFunction(e, num);
});
Or if you really want to be object oriented (and use a neat capability of jQuery), you can save the index value as a separate data item associated with the object and retrieve it from there later.
$('#box_'+i).data("index", i).bind('mousedown',function(e){
if(e.which == 3) theFunction(e, $(this).data("index"));
});
Working demo of the second method here: http://jsfiddle.net/jfriend00/wVDHw/
for(var i=0;i<100;i++)
(function(i){
$('#box_'+i).bind('mousedown',function(e){
if(e.which==3)
theFunction(e,i);
});
})(i);

How to pass a variable by value to an anonymous javascript function?

The Objective
I want to dynamically assign event handlers to some divs on pages throughout a site.
My Method
Im using jQuery to bind anonymous functions as handlers for selected div events.
The Problem
The code iterates an array of div names and associated urls. The div name is used to set the binding target i.e. attach this event handler to this div event.
While the event handlers are successfully bound to each of the div events, the actions triggered by those event handlers only ever target the last item in the array.
So the idea is that if the user mouses over a given div, it should run a slide-out animation for that div. But instead, mousing over div1 (rangeTabAll) triggers a slide-out animation for div4 (rangeTabThm). The same is true for divs 2, 3, etc. The order is unimportant. Change the array elements around and events will always target the last element in the array, div4.
My Code - (Uses jQuery)
var curTab, curDiv;
var inlineRangeNavUrls=[['rangeTabAll','range_all.html'],['rangeTabRem','range_remedial.html'],
['rangeTabGym','range_gym.html'],['rangeTabThm','range_thermal.html']];
for (var i=0;i<inlineRangeNavUrls.length;i++)
{
curTab=(inlineRangeNavUrls[i][0]).toString();
curDiv='#' + curTab;
if ($(curDiv).length)
{
$(curDiv).bind("mouseover", function(){showHideRangeSlidingTabs(curTab, true);} );
$(curDiv).bind("mouseout", function(){showHideRangeSlidingTabs(curTab, false);} );
}
}
My Theory
I'm either not seeing a blindingly obvious syntax error or its a pass by reference problem.
Initially i had the following statement to set the value of curTab:
curTab=inlineRangeNavUrls[i][0];
So when the problem occured i figured that as i changed (via for loop iteration) the reference to curTab, i was in fact changing the reference for all previous anonymous function event handlers to the new curTab value as well.... which is why event handlers always targeted the last div.
So what i really needed to do was pass the curTab value to the anonymous function event handlers not the curTab object reference.
So i thought:
curTab=(inlineRangeNavUrls[i][0]).toString();
would fix the problem, but it doesn't. Same deal. So clearly im missing some key, and probably very basic, knowledge regarding the problem. Thanks.
You need to create a new variable on each pass through the loop, so that it'll get captured in the closures you're creating for the event handlers.
However, merely moving the variable declaration into the loop won't accomplish this, because JavaScript doesn't introduce a new scope for arbitrary blocks.
One easy way to force the introduction of a new scope is to use another anonymous function:
for (var i=0;i<inlineRangeNavUrls.length;i++)
{
curDiv='#' + inlineRangeNavUrls[i][1];
if ($(curDiv).length)
{
(function(curTab)
{
$(curDiv).bind("mouseover", function(){showHideRangeSlidingTabs(curTab, true);} );
$(curDiv).bind("mouseout", function(){showHideRangeSlidingTabs(curTab, false);} );
})(inlineRangeNavUrls[i][0]); // pass as argument to anonymous function - this will introduce a new scope
}
}
As Jason suggests, you can actually clean this up quite a bit using jQuery's built-in hover() function:
for (var i=0;i<inlineRangeNavUrls.length;i++)
{
(function(curTab) // introduce a new scope
{
$('#' + inlineRangeNavUrls[i][1])
.hover(
function(){showHideRangeSlidingTabs(curTab, true);},
function(){showHideRangeSlidingTabs(curTab, false);}
);
// establish per-loop variable by passsing as argument to anonymous function
})(inlineRangeNavUrls[i][0]);
}
what's going on here is that your anonmymous functions are forming a closure, and taking their outer scope with them. That means that when you reference curTab inside your anomymous function, when the event handler runs that function, it's going to look up the current value of curTab in your outer scope. That will be whatever you last assigned to curTab. (not what was assigned at the time you binded the function)
what you need to do is change this:
$(curDiv).bind("mouseover", function(){showHideRangeSlidingTabs(curTab, true);} );
to this:
$(curDiv).bind("mouseover",
(function (mylocalvariable) {
return function(){
showHideRangeSlidingTabs(mylocalvariable, true);
}
})(curTab)
);
this will copy the value of curTab into the scope of the outer function, which the inner function will take with it. This copying happens at the same time that you're binding the inner function to the event handler, so "mylocalvariable" reflects the value of curTab at that time. Then next time around the loop, a new outer function, with a new scope will be created, and the next value of curTab copied into it.
shog9's answer accomplishes basically the same thing, but his code is a little more austere.
it's kinda complicated, but it makes sense if you think about it. Closures are weird.
edit: oops, forgot to return the inner function. Fixed.
I think you're making this more complicated than it needs to be. If all you're doing is assigning a sliding effect on mouseover/out then try the hover effect with jquery.
$("#mytab").hover(function(){
$(this).next("div").slideDown("fast");},
function(){
$(this).next("div").slideUp("fast");
});
If you posted your full HTML I could tell you exactly how to do it :)
You can put your variable's value into a non existing tag, and later you can read them from there. This snippet is part of a loop body:
s = introduction.introductions[page * 6 + i][0]; //The variables content
$('#intro_img_'+i).attr('tag' , s); //Store them in a tag named tag
$('#intro_img_'+i).click( function() {introduction.selectTemplate(this, $(this).attr('tag'));} ); //retrieve the stored data

Categories