For loop function, unexpected output - always the same [duplicate] - javascript

This question already has answers here:
JavaScript closure inside loops – simple practical example
(44 answers)
Javascript infamous Loop issue? [duplicate]
(5 answers)
Closed 8 years ago.
I have this weird situation and I don't know why do the output is not what I expect. This is only a simple for-loop function. Can somebody explain me why this happens?
var pm = 2;
for (var i = 0; i < pm; i++) {
$("#specialty_pm_"+i).mouseenter(function(){
alert(i);
});
};
I have 2 divs in my html that has id="specialty_pm_<?php echo $countPM; ?>" and that div is inside a for loop function in php. foreach ($employee_info as $emp_info){ $countPM++; }
I expect that the alert on hover in the 1st div is '1' and the 2nd div is '2'. but when I hover the 1st div, it will alert '2'.

you should use JavaScript closure:
var pm = 2;
for (var i = 0; i < pm; i++) {
var func = (function(i){
return function(){
alert(i);
}
})(i);
$("#specialty_pm_"+i).mouseenter(func);
};
The point is in your code all the mouseenter functions use the same variable i, and after your loop ends, it has its last value which is 2. Using a scope chain, with nested functions and their closures in JavaScript, you can create safe scopes for your variables. Basically what nested functions do, is to provide a outer LexicalEnvironment for the inner function. You can find more information in this post:
Scope Chain in Javascript.

You alert can't wirks because i has only one instance.
Yoi can check variable i inside your div in this case.
try this:
$("#specialty_pm_"+i).mouseenter(function(){
var id = $(this).attr('id');
alert(id.substring(0,13));
});

The reason, as already mentioned, is that the scope of i is the same for both eventhandlers, and as such it will have the same value for both of them.
There is a couple of solution for this problem.
Solution 1: create a new scope via a immediate function
var pm = 2;
for (var i = 0; i < pm; i++) {
$("#specialty_pm_"+i).mouseenter(function(instance){
return function() { alert(instance); };
}(i));
};
You can see a fiddle of it in action here: http://jsfiddle.net/LP6ZQ/
Solution 2: use jQuerys data method to store the value
var pm = 2;
for (var i = 0; i < pm; i++) {
$("#specialty_pm_"+i).data('instance',i).mouseenter(function(){
alert($(this).data('instance'));
});
};
You can see a fiddle of it in action here: http://jsfiddle.net/LP6ZQ/1/
Solution 3: bind the instance number to the eventhandler
var pm = 2;
for (var i = 0; i < pm; i++) {
$("#specialty_pm_"+i).mouseenter(function(instance){
alert(instance);
}.bind(null,i));
};
You can see a fiddle of it in action here: http://jsfiddle.net/LP6ZQ/2/
Solution 3 has a few caveats - this is being bound as null, and thus it can no longer be used as a reference to the dom element, like jQuery eventhandlers nomrally do. Also bind isn't supported by older browsers, but this can be mitigated by usinga polyfill, a good one can be found here: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind
Solution 4: Be smart and use a delegate instead of binding event handlers in a loop
$(document.body).on('mouseenter','.specialty_pm',function(){
alert($(this).data('id'));
});
You can see a fiddle of it in action here: http://jsfiddle.net/LP6ZQ/4/
Solution 4 is the "right way" to do it, but it will require you to change the way you build your markup

You could get rid of the for loop in your JavaScript and use jQuery's Starts With Selector to select ALL elements whose id begins with 'specialty_pm_'
$("[id^='specialty_pm_']").on("mouseenter",function(){
var id = $(this).attr('id');
alert(id.substring(13));
});
E.G: http://jsfiddle.net/6bTJ3/
Alternatively you could add a class to each of these specialty_pm_[n] items to make it even simpler to select them in jQuery.

Related

Why is for loop setting all jquery onclick to last iteration [duplicate]

This question already has answers here:
JavaScript closure inside loops – simple practical example
(44 answers)
Closed 5 years ago.
I have the following code that works
var btns = $('.gotobtn');
$('#'+btns.get(0).id).click(function() {
document.querySelector('#navigator').pushPage('directions.html', myInfo[0]); });
$('#'+btns.get(1).id).click(function() {
document.querySelector('#navigator').pushPage('directions.html', myInfo[1]); });
$('#'+btns.get(2).id).click(function() {
document.querySelector('#navigator').pushPage('directions.html', myInfo[2]); });
// this works. I click on button 0 and get myInfo[0],
// on 1 and get myInfo[1], on 2 and get myInfo[2]
But replacing it with a loop does not work correctly. Instead, I always get the last element: myIfno[2] for any button I press.
var btns = $('.gotobtn');
var i = 0;
for (i = 0; i<3; i++){
var btnid = "#" + btns.get(i).id;
$(btnid).click(function() {
document.querySelector('#navigator').pushPage('directions.html', myInfo[i]); });
}
// this does set the buttons on-click but when I click on them,
// all get the latest iteration, in this example myInfo[2]
Why is this? And how do I fix that, without defining each button manually?
I want to see how to do it in jquery.
Because: JavaScript does not have block scope. Variables introduced with a block are scoped to the containing function or script
Replace:
$(btnid).click(function() {
document.querySelector('#navigator').pushPage('directions.html', myInfo[i]);
});
With:
$(btnid).click(customFunction(i));
And declare this function outside the loop:
function customFunction(i) {
document.querySelector('#navigator').pushPage('directions.html', myInfo[i]);
}
your
$(btnid).click(function() {
document.querySelector('#navigator').pushPage('directions.html', myInfo[i]); });
Must be outside your for loop.
#ibrahim mahrir is correct. It's caused by the same phenomena as described in 46039325 although this question is specific for JQuery binding and will probably be useful to some. (I saw the unanswered question in several places on the web)
It happens because I'm binding to i, and in the meantime i has changed to the last iteration (which is 2 in this example). I need to bind to the value of i while iterating.
This will happen (due to quirks in javascript) if I define the binding to a parameter of a function. The parameter is "dynamically created" each time and the value of that param (during that iteration) will be bound.
So when I finally do click on the second button (id:1, the first is id:0), it will invoke the method with the value of 1, correctly.
Here's an example of how the fix looks in jQuery:
$(function(){ // document ready
function btnaction(i){
var btns = $('.gotobtn');
$('#'+btns.get(i).id).click(function() {
document.querySelector('#navigator').pushPage('directions.html', gotoInfo[i]);
});
}
and I call it in the loop
for (i = 0; i<6; i++)
btnaction(i);
Alls well that ends well...

Trying to make sense of "this" in my javascript code (one thing works, the other doesn't)

I've been trying to learn javascript by refactoring some Jquery examples in a book into javascript. In the following code I add a click listener to a tab and make it change to active when the user clicks on the tab.
var tabs = document.querySelectorAll(".tabs a span");
var content = document.querySelectorAll("main .content li");
for (var tabNumber = 0; tabNumber <= 2; tabNumber++) {
tabs[tabNumber].addEventListener("click", function (event) {
for (var i = 0; i < tabs.length; i++) {
tabs[i].classList.remove("active");
}
tabs[tabNumber].classList.add("active");
for (var i = 0; i < content.length; i++) {
content[i].innerHTML = "";
}
event.preventDefault();
});
}
This returns an undefined error when I run it. However, I tried replacing tabs[tabNumber].classList.add("active") with this.classList.add("active") and it worked.
Why doesn't the previous code work? As far as I can see they are referring to the same thing, and tabs[tabNumber] should work since at that point in the code it is tabs[0].
If use this, I think it's better and a more polished solution. If you still want to use tabNumber, it's probably evaluating to 3 in every click callback, because it's the number after the last iteration, and you don't have a tabs[3] position.
So, you just have to make a closure of the tabNumber variable.
I guess other answers told you why tabs[tabNumber] does not work (because it comes from the score of the for loop and so, is always equal to the greater value of tabNumber).
That's why I would recommend using a .forEach loop. Careful though because it doesn't work on arrays of DOM nodes produced by document.querySelectorAll(), but you can use:
// ES6
Array.from(document.querySelectorAll('...'))
// ES5
[].slice.call(document.querySelectorAll('...'))
Anyway, I made a simplified working demo of your code.
Note that I save the currently active tab in a variable, to prevent another for loop. You could also do:
document.querySelector('.active').classList.remove('active')
But I like to reduce the amount of DOM reading.
Good luck for your apprentissage, re-writing some jQuery code into Vanilla JS seems like a good method, and you might acquire a far better comprehension of JavaScript.

I'm trying to consolidate my code...why can't I do this? [duplicate]

This question already has answers here:
JavaScript closure inside loops – simple practical example
(44 answers)
Closed 9 years ago.
If you can't tell by my other questions, I'm a self-taught newb. I'm trying to consolidate some of my js code. I have 2 divs, with class a0 and a1. I'd like to attach a mouseenter event to each (same event).
Here's my fiddle, which is (hopefully) self-explanatory. .a0 and .a1 are my original code and .f0 and .f1 represents my (failed) attempt to consolidate. Why can't I do a "for" loop (or, if I can, why do I always end up w/ value of "2" for i?)?
(If there's a way I can do it in jquery, that's fine)
See above fiddle for full demo:
$(".a0").on("mouseenter",function(){
$(this).html("value: 0");
});
$(".a1").on("mouseenter",function(){
$(this).html("value: 1");
});
/* my failed attempt to consolidate the above code */
for (var i=0; i<2; i++){
$(".f"+i).on("mouseenter",function(){
$(this).html("value: "+ i);
});
}
Quick & Dirty
$(".a0, .a1").on("mouseenter",function(){
$(this).html("value: " + ($(this).hasClass('a0') ? 0 : 1));
});
More elegant
To resolve your question in a more elegant way, let's first understand why your code does not work.
You are defining two callbacks that will read into the value of i.
However, i will be already set to 2 when your mouse will enter the element.
You can use a closure to prevent this :
for (var i=0; i<2; i++){
(function(j) {
$(".f"+i).on("mouseenter",function(){
$(this).html("value: "+ j);
});
})(i);
}
In this case, the value of i is captured into j and won't be changed when the callback is called.
Here is a Working fiddle
The problem is that i is a reference, and by the time your handler is called it is === 2
The following works for me.
var assignME = function (i) {
$(".f"+i).on("mouseenter",function(){
$(this).html("value: "+ i);
});
};
for (var i=0; i<2; i++){
assignME(i);
}

Javascript Array addEventListener

Interactive map with buttons in the shape of states, each button has the state abbreviation as an id, when a button/state is clicked I would like to fire the function "stateSelect" and send the state abbreviation with it so I know what's been pressed. Why doesn't the following work?
var stateList = new Array("AK","AL","AR","AS","AZ","CA","CO","CT","DC","DE","FL","GA","GU","HI","IA","ID",
"IL","IN","KS","KY","LA","MA","MD","ME","MH","MI","MN","MO","MS","MT","NC","ND","NE","NH","NJ","NM","NV","NY",
"OH","OK","OR","PA","PR","PW","RI","SC","SD","TN","TX","UT","VA","VI","VT","WA","WI","WV","WY");
for (var i = 0; i < stateList.length; i++) {
document.getElementById(stateList[i]).addEventListener('mousedown', function() {stateSelect(stateList[i])}, false);
}
I obviously want to avoid 50 some lines of code but I'm not sure why this simple loop isn't working.
Because when the handler runs, it looks up the value of i, which is wherever it was after the loop finished.
You need to scope the i variable in a function:
function listenerForI( i ) {
document.getElementById(stateList[i]).addEventListener('mousedown', function() {stateSelect(stateList[i])}, false);
}
for (var i = 0; i < stateList.length; i++) {
listenerForI( i );
}
Now the i referenced by the handler will be the parameter to the listenerForI function that was invoked. As such, that i will reference the value that was passed in from the for loop.
You have a scoping issue. Javascript is not block-scoped; it is function-scoped. Basically, you must create a new function whenever you wish to create a new variable in a loop.
The most elegant way to do so is as follows:
stateList.map(function(abbrev){
$(abbrev).mousedown(function(){stateSelect(abbrev)});
});
If you are not using jQuery, merely replace $(abbrev).mousedown with document.getElementById(abbrev).addEventListener.
(Just to preempt the people who go "map isn't standard"; it is in the javascript ECMA-262 standard 5th edition which has support from all browser vendors. If one is paranoid about supporting older browsers, one can just $.map.)
Here is how one would do so using a for loop; it's a bit uglier but it demonstrates the necessity of creating new closures via functions:
for(var i=0; i<stateList.length; i++)
(function(i){
$(stateList[i]).mousedown(...);
})(i);
Like I said, a bit uglier than necessary; you could also do this which is slightly less ugly, but is basically the same thing:
function createListener(abbrev) {
$(abbrev).mousedown(...);
}
for(var i=0; i<stateList.length; i++)
createListener(stateList[i]);

Dynamically Change HTML DOM event

I am trying to dynamically change an element's onClick event and I have something like the following:
for (var i = 1; i < 5; i++)
{
getElementById('element' + i).onclick = function() { existingFunction(i); return false; };
}
Everything seems to work fine apart from the fact that the argument passed to 'existingFunction()' is the final value of i=4 each time it is called. Is there a way to bind a function to onclick that uses the value of i at the time of binding as opposed to what it seems to be doing at the moment and referencing the original i in the for-loop.
Also is is there a way of performing the same bind without having to create anonymous functions each time? so that I can directly reference 'existingFunction' in each onclick for performance reasons?
Cheers guys,
Yong
Change
for (var i = 1; i < 5; i++)
{
getElementById('element' + i).onclick = function() { existingFunction(i); return false; };
}
to
for (var i = 1; i < 5; i++)
{
getElementById('element' + i).onclick = createOneHandler(i);
}
function createOneHandler(number){
return function() {
existingFunction(number);
}
}
and it should work fine.
Working Demo
A good explanation is given here
JavaScript, time to grok closures
for the i being always 4, you have a scoping problem, I advise to read this. Scoping is are really important concept, so you have better to make sure to understand what's is going on.
a better code would be
for (var i = 1; i < 5; i++)
{
getElementById('element' + i).onclick = existingFunction;
}
the onclick would pass an event has argument so you can know what element have been clicked
i.e.
function existingFunction(event){
// DO something here
}
you can read more about events there. IE does have the exact same event model as other browser so you would have to handle it.
Last bit, I advise you to use a JS framework(Jquery,ExtJS,DOJO,Prototype...) because it would simplify your task
the code you posted should work the way you intended, your problem with i=4 is elsewhere. edit: this is wrong, rageZ is right about the scoping problem.
re the other question: all you can do is offload the verbosity with
var f = function (i) { return function () { existingFunction(i); return false; } }
for (...) { document.getElementById(...).onclick = f(i); }
BTW, you should use something like jQuery for DOM manipulation (concise syntax), and perhaps Zeta (http://codex.sigpipe.cz/zeta/) for the function composition
var f = compose(false_, existingFunction);
for (...) { $(...).click(f(i));
Hooray! It's loop closures again! See 422784, 643542, 1552941 et al for some more discussion.
is there a way of performing the same bind without having to create anonymous functions each time?
Yes, in ECMAScript Fifth Edition you get function.bind:
for (var i = 1; i < 5; i++)
document.getElementById('element'+i).onclick= existingFunction.bind(window, i);
In the meantime since browsers don't yet generally support it you can monkey-patch an alternative implementation of bind (see the bottom of this comment for one such) built out of anonymous functions as a fallback.
Alternatively, assign the same event handler function to every element and just have it look at this.id to see which element number it is.

Categories

Resources