Difference between closures in loops in JavaScript - javascript

What is the difference between these functions? Why does the first one work and the second does not work as expected?
http://jsfiddle.net/GKDev/x6pyg/ (this works)
http://jsfiddle.net/GKDev/bv4em/ (and this is not)
I'm trying to loop over input elements and add onfocus events on them:
for (var i = 0; i < helpText.length; i++) {
var item = helpText[i];
document.getElementById(item.id).onfocus = function() {
showHelp(item.help);
};
}

In your non-working example, when the anonymous function is called, item has the last value it held when the for loop finished executing. That's because this variable belongs to the parent function that contains the for loop.
In your working example, you create a new function, pass in the current value of item like so:
function (help) {
return function () {
showHelp(help); // <-- This will be the value enclosed in this anonymous function
};
}(item.help); // <-- Calls an anonymous function passing in the current value
This creates a new closure around that value as it existed during that iteration. When the anonymous function is called, it uses that local value.

It is treated as:
var item;
for (var i = 0; i < helpText.length; i++) {
item = helpText[i];
document.getElementById(item.id).onfocus = function() {
showHelp(item.help);
};
}
The loop has finished before any focus callback fires and at that point item is the last item assigned.

It is pretty easy in fact.
In the first case you are passing item.help inside of the closure, which acts as a local copy. It's like a prisoner of its scope. What happens outside, nobody cares.
for (var i = 0; i < helpText.length; i++) {
var item = helpText[i];
document.getElementById(item.id).onfocus = function (help) {
return function () {
showHelp(help);
};
}(item.help);
}
In the second one, there is no closure preserving the value of item, which means that when item is evaluated, it evaluates to its actual value, i.e.: the last element of the array, since the last loop of the for set its value to the last value of the array.
for (var i = 0; i < helpText.length; i++) {
var item = helpText[i];
document.getElementById(item.id).onfocus = function() {
showHelp(item.help);
}
}

Related

The role of the (i) and (index) on this function

i have this code and this is working:
var wrap_inner = document.getElementsByClassName("wrapper"),
base_icon = document.getElementsByClassName("inner");
function fx_effect () {
for (var i = 0; i < wrap_inner.length; i++) {
(function (index) {
wrap_inner[i].addEventListener('click',function() {
base_icon[index].classList.toggle('tc');
});
})(i); <==== what is that? what do that?
}
}fx_effect();
but i don't know what is the index in: function (index) { ...
and what is (i) at the end of function.
Why index is used? What is its value?
why (i) at the end of function used like this?
When should be used in this way?
why the function like this don't work???:
function fx_effect () {
for (var i = 0; i < wrap_inner.length; i++) {
wrap_inner[i].addEventListener('click',function() {
base_icon[i].classList.toggle('tc');
});
}
}fx_effect();
I'm confused :(
The concept is javascript closures . First method is creating a closure ( a scope or a stack as analogy) for each value passed. So that when fx_effect is called, the inner method runs for values 0 to wran_inner.length distinctively as each value is in a different closure.
For second method, the scope will have just the final value, hence calling the method fx_effect will result in the loop running that method for final value of i, i.e. wrap_inner.length - 1

function receives value as undefined

I have a timeout which calls a function until 100% progress is complete. It then executes the function I have assigned to it. Only the value that was given to it is undefined or at least part of it.
I'm not sure at which stage the code is losing the value being passed, thus making it return undefined but I have made a JS Fiddle with it in action for you to see it:
JS Fiddle
My end result is to receive the value correct then remove the given element like so:
function rmv_div(div_id) {
//div_id is not properly defined so cannot find the div.
document.getElementById('result').innerHTML = div_id;
var div = document.getElementById(div_id);
div.parentNode.removeChild(div);
}
The problem is that the variable i used inside func is created outside the scope of that function, and is increased at each iteration. Then, when you call func at the end, i equals array.length, so array[i] is undefined.
You can solve it creating another variable at each iteration that you won't increase:
Solution 1:
Demo: http://jsfiddle.net/qJ42h/4/ http://jsfiddle.net/qJ42h/11/
for (var i = 0; i < array.length; i++) {
var bar = document.getElementById('bar' + array[i]),
text = document.getElementById('text' + array[i]),
remove = 'wrap' + array[i],
j = i;
do_something(bar, text, function () {
rmv_div('id' + array[j]);
}, 1);
}
Solution 2
Demo: http://jsfiddle.net/qJ42h/8/ http://jsfiddle.net/qJ42h/12/
for (var i = 0; i < array.length; i++) {
var bar = document.getElementById('bar' + array[i]),
text = document.getElementById('text' + array[i]),
remove = 'wrap' + array[i];
do_something(bar, text, (function(i) {
return function(){ rmv_div('id' + array[i]); }
})(i), 1);
}
The problem here is that you didn't isolate the loop variable i inside the closure. However, this can be solved much more elegantly by using objects.
First off, I'm introducing the object that will encapsulate what you want; it gets initialized with a bar element and a function to call when it's done counting to 100. So, I'll call it BarCounter:
function BarCounter(element, fn)
{
this.element = element;
this.fn = fn;
this.text = element.getElementsByTagName('div')[0];
this.counter = 0;
}
This is just the constructor; it doesn't do anything useful; it resolves the text element, which is simply the first <div> tag it can find underneath the given element and stores that reference for later use.
Now we need a function that will do the work; let's call it run():
BarCounter.prototype.run = function()
{
var that = this;
if (this.counter < 100) {
this.text.innerHTML = this.counter++;
setTimeout(function() {
that.run();
}, 70);
} else {
this.fn(this.element);
}
}
The function will check whether the counter has reached 100 yet; until then it will update the text element with the current value, increase the counter and then call itself again after 70 msec. You can see how the reference to this is kept beforehand to retain the context in which the run() function is called later.
When all is done, it calls the completion function, passing in the element on which the BarCounter object operates.
The completion function is much easier if you pass the element to remove:
function removeDiv(element)
{
element.parentNode.removeChild(element);
}
The final step is to adjust the rest of your code:
var array = [1];
for (var i = 0; i < array.length; ++i) {
var bar = new BarCounter(
document.getElementById('bar' + array[i]),
removeDiv
);
bar.run();
}
It's very simple now; it creates a new BarCounter object and invokes its run() method. Done :)
Btw, you have the option to remove the element from within the object as well; this, of course, depends on your own needs.
Demo

Wrong Parameter is passed to function

I have the following code that adds an onmouseover event to a bullet onload
for (var i = 0; i <= 3; i++) {
document.getElementById('menu').getElementsByTagName('li')[i].onmouseover = function () { addBarOnHover(i); };
}
This is the function that it is calling. It is supposed to add a css class to the menu item as the mouse goes over it.
function addBarOnHover(node) {
document.getElementById('menu').getElementsByTagName('li')[node].className = "current_page_item"; }
When the function is called, I keep getting the error:
"document.getElementById("menu").getElementsByTagName("li")[node] is
undefined"
The thing that is stumping me is I added an alert(node) statement to the addBarOnHover function to see what the value of the parameter was. The alert said the value of the parameter being passed was 4. I'm not sure how this could happen with the loop I have set up.
Any help would be much appreciated.
This is a common problem when you close over an iteration variable. Wrap the for body in an extra method to capture the value of the iteration variable:
for (var i = 0; i <= 3; i++) {
(function(i){ //here
document.getElementById('menu').getElementsByTagName('li')[i].onmouseover = function () { addBarOnHover(i); };
})(i); //here
}
an anonymous function is created each time the loop is entered, and it is passed the current value of the iteration variable. i inside the anonymous function refers to the argument of this function, rather than the i in the outer scope.
You could also rename the inner variable for clarity:
for(var i=0; i<=3; i++){
(function(ii){
//use ii as i
})(i)
}
Without capturing the iteration variable, the value of i when it is finally used in the anonymous handler has been already changed to 4. There's only one i in the outer scope, shared between all instances of the handler. If you capture the value by an anonymous function, then the argument to that function is used instead.
i is being passed as a reference (not by value), so once the onmouseover callback is called, the value of i has already become 4.
You'll have to create your callback function using another function:
var menu = document.getElementById('menu');
var items = menu.getElementsByTagName('li');
for (var i = 0; i <= 3; i++) {
items[i].onmouseover = (function(i) {
return function() {
addBarOnHover(i);
};
})(i);
}
You could make it a little more readable by making a helper function:
var createCallback = function(i) {
return function() {
addBarOnHover(i);
};
};
for (var i = 0; i <= 3; i++) {
items[i].onmouseover = createCallback(i);
}

Why does my anonymous function not have the context of the current object in this example?

When I run this sample code in Google Chrome, the intended behavior--loading an image within a placeholder image tag on the current page--does not occur. I checked the value of currPic when showPic() is called, and it is "undefined." I know if I change the parameter to showPic from 'anchors[i]' to 'this', then it will work, but was trying to understand why this is so.
function showPic(currPic) {
var srcLoc = currPic.getAttribute("href");
var placeHolder = document.getElementById("placeholder");
placeHolder.setAttribute("src", srcLoc);
var imgLabel = document.getElementById("imglabel");
var currLinkTitle = currPic.getAttribute("title");
imgLabel.firstChild.nodeValue = currLinkTitle;
}
function prepareGallery() {
if(!(document.getElementsByTagName && document.getElementById)) return false;
var imgGallery = document.getElementById("imagegallery");
if(imgGallery) {
var anchors = imgGallery.getElementsByTagName("a");
var i;
for(i = 0; i < anchors.length; i++) {
anchors[i].onclick = function() {
showPic(anchors[i]);
return false;
}
}
}
}
Inside the anonymous function, anchors[i] provides a runtime reference. At the time the click occurs, anchors[i] no longer exists. While it existed at the time the assignment was made, it falls out of scope at the time of the click (since it's just an array reference). However, using this provides a solid reference to the immediate object that is always available at the time of the click.
More succinctly, anchors[i] is a reference to a position in an array (which leaves scope once the for loop exits). this is a reference to the dom element itself.
Because this would also work: showPic(document.getElementById(anchors[i].id)); - do you "get" it now (pun very much intended)?
Didn't see the obvious statement regarding how closures work, so here's my take on it.
var i;
for(i = 0; i < anchors.length; i++) {
anchors[i].onclick = function() {
showPic(anchors[i]);
return false;
}
}
Notice how you reference the i variable inside the loop? By the end of your loop, the value of i equals anchors.length.
So, when any of your onclick function is executed, that reference to i now points one position past the last index of anchors; this is why you see currPic is undefined.
One solution to this problem has been given in other answers: use this to reference the current anchor and don't pass anchors[i] to the onclick function.
As you may encounter similar situations, I'll show you another solution by closing over the value of i like so:
var i;
for(i = 0; i < anchors.length; i++) {
anchors[i].onclick = (function(i) {
// inside this function, i is closed over and won't change anymore
return function() {
showPic(anchors[i]);
return false;
}
}(i));
}

Deleting of an item, indexing problem...

for (var i = 0; i < max; i++) {
deleteButton[i].addEventListener('click', function (e) {
AddPatientVitalsModel.globalData.splice(i, 1);
});
}
When i click on a button, a new table is added... i change some information in table and again add another table... and i delete this table... but the array value does delete the first index alone.
The Value of i is always zero and it deletes the first item in the Array. How can i ensure that for each table it has different ID.
For each click i feel so that i value always starts from Zero....
You're overwriting the value of i at each loop. You could use an anonymous function to pass the variable by value:
for (var i = 0; i < max; i++) {
(function(i){ //i is now defined within the scope of this anonymous function
deleteButton[i].addEventListener('click', function (e) {
AddPatientVitalsModel.globalData.splice(i, 1);
}, true);
})(i); //Pass i to the anonymous function
}
Note that i can have a different name inside the anonymous function, without making a difference.
ANother notice: Pass three arguments to addEventListener, because many browsers will throw an error if the third argument is omitted.
Because i scope is outside event. You need get index from variable e, or maybe work this code:
for (var i = 0; i < max; i++) {
var ii = i;
deleteButton[i].addEventListener('click', function (e) {
AddPatientVitalsModel.globalData.splice(ii, 1);
});
}
But I am not sure it works on javascript.

Categories