weird javascript problem - javascript

I'm dynamically inserting some html into the document (by using obj.innerHTML += 'some html'). In that html, there are images with 'imageId_X' ids (ie. imageId_1, imageId_2...). This works fine, but there's something wrong with the following code:
for (var n = 0; n < pConfig.images.length; n++)
{
document.getElementById('imageId_' + n).onclick = function()
{
alert(n);
}
}
There are 4 elements in the pConfig.images and alert(n) always alerts 4. Why is this happening, what am i doing wrong?

The cause of your problem is lamba expression in your code. When you define your anonymous function as onclick handler you bound it for outer variable n, which at the end of the loop is always 4 that the reason you get it always 4.
To do it the way you have planned it to be you need to do the following:
for (var n = 0; n < pConfig.images.length; n++)
{
function( n) {
document.getElementById('imageId_' + n).onclick = function()
{
alert(n);
}
}( n);
}
Hence you define separate scope for variable.

The problem is that each function you create has a reference to the same n variable - the one that is incremented. By the end of the loop, it is at 4 - and all functions you made refer to that variable with that value.
You can work around it with a closure, e.g.:
function closure(n) {
return function() {alert(n);}
}
for (var n = 0; n < pConfig.images.length; n++)
{
document.getElementById('imageId_' + n).onclick = closure(n);
}

Looks like you need a closure. See How do JavaScript closures work?

Related

JavaScript - Why does this closure not hold parameters?

I'm reading an article on how closures work. I'm understanding almost all of it except for Section 5 in that article.
It's talking about how closures work with loops with this example:
function buildList(list) {
var result = [];
for (var i = 0; i < list.length; i++) {
var item = 'item' + list[i];
result.push( function() {alert(item + ' ' + list[i])} );
}
return result;
}
function testList() {
var fnlist = buildList([1,2,3]);
// using j only to help prevent confusion - could use i
for (var j = 0; j < fnlist.length; j++) {
fnlist[j]();
}
}
testList();
Three alerts come up, each saying item3 undefined. I do understand why it says item 3. The closure from the buildList function holds a reference to item, it doesn't hold the actual value.
What I don't get is why list[i] comes back as undefined in each alert from the result.push... line. Doesn't that closure still have the list parameter from retained?
Doesn't that closure still have the list parameter from retained?
Yes. But the value of i will be equal to list.length and list[list.length] is undefined. If an array has length 3, then the highest accessible index is 2, not 3:
var arr = [1,2,3];
console.log(arr[2]); // 3
console.log(arr[3]); // undefined
Related: JavaScript closure inside loops – simple practical example
buildList and it's inner, anonymous function share context to a certain degree. Changing i, as is done in the for loop, changes it for all the created anonymous functions that are created even after the fact. Change the anonymous function from:
function() {alert(item + ' ' + list[i])}
to something like:
(function(i){return function() {alert(item + ' ' + list[i]);};})(i)
This causes a copy of i's value to be made at the time of creation and assigned into another, different i that overrides the outer i in the context of the returned anonymous function, while still allowing dynamic access to item and list.

Javascript function with dynamically generated arguments

Below code :
loop(n times)
create HTML Button Element
count++;
assign onclick event = function(){
openSomething("Value_"+count)
}
so if i create 3 input elements (n=3) and then go back click any of the three buttons then every time openSomething("Value_"+3) only gets called.
why openSomething("Value_"+1) and openSomething("Value_"+2) does not get called?
I am not sure what is going on may be it the scope issue but i dont know much about scope either, any help to push me in the right direction is much appreciated.
My original code
var count = 0;
for(var i =0;i<someValue;i++){
count++;
var button = document.createElement("img");
button.src = "/images/small_button.gif";
button.imageButton = true;
button.srcBase = "/images/small_button";
button.onclick = function () {
selectSomething("someIdText_"+count);};
cell.appendChild(button);
}
Because JavaScript doesn't have block-level scoping of variables, and as a result everything is scoped to the function. That means that when you have code that uses a variable (like your loop counter n or your count variable) at a later point (i.e. after the full execution of the function), it will have its value set to the last value for the loop. You need to create a closure (a new scope for the variable) inside of your loop. Something like this (since you didn't post your actual code):
for(var i = 0, l = list.length; i < l; i++) {
(function(count) {
something.onclick = function() {
openSomething("Value_" + count);
}
})(i);
}
For a more modern approtce use let,
works for firefox, chrome, and node
if you need to target all the browsers, use Anthony approach
for(var count = 0, l = list.length; count < l; count++) {
let count;
something.onclick = function() {
openSomething("Value_" + count);
}
}

Can someone please explain to me how the scope works for this array of anonymous functions?

Quite simply, I'd like to know why the call to arr0 seems to drag in the value of i instead of the one stored in the function at that position.
<script>
var arr = [];
for(var i = 0; i < 3; i++) {
//Assign anonymous functions to the array in positions 0 to 2
arr[i] = function() { console.log("function " + i); }
}
for(var i = 0; i < 3; i++) {
//The output for these function calls is correct!
arr[i]();
}
//Here I expected to output: function 0, but instead outputs: function 3 WTF!
arr[0] ();
</script>
Here's the output:
function 0
function 1
function 2
function 3
For the last call, i.e: arr[ 0 ] (); I expected the output to be "function 0", but surprisingly IT'S NOT... Could someone please care to explain why?
Thanks in advance!
Well this is a mixed bunch...
You are using the same i variable (despite "re-defining" it in the second loop, it's still the same i) that is placed in the global scope
As a result, in the second loop, each iteration alters the value of that global i, which results in the
function 0
function 1
function 2
function 3
output.
It was, by the way, absolutely not the expected result, if you used k in the second loop:
<script>
var arr = [];
for(var i = 0; i < 3; i++) {
//Assign anonymous functions to the array in positions 0 to 2
arr[i] = function() { console.log("function " + i); }
}
for(var k = 0; k < 3; k++) {
//The output for these function calls is correct!
arr[k]();
}
//Here I expected to output: function 0, but instead outputs: function 3 WTF!
arr[0] ();
</script>
That would produce:
function 3
function 3
function 3
function 3
and that is the infamous loop problem referred in the link above (in comments).
The reason is that functions, defined in your first loop (which, BTW, you should try to avoid in general case), "close" on the variables that are in the same scope as their definition - i in this case. That means that whenever value of i is changed later on, it will be reflected in that function.
The last example shows it in action - i is changed by the first for loop, so when the loop is finished - it's value is 3. All functions you defined now have the same value of i - 3.
To make the output like this:
function 0
function 1
function 2
function 0
you can do this (not that it's that great, structure wise):
var arr = [];
for(var i = 0; i < 3; i++) {
//Assign anonymous functions to the array in positions 0 to 2
arr[i] = (function(index){ return function() { console.log("function " + index); };}(i));
}
for(var k = 0; k < 3; k++) {
//The output for these function calls is correct!
arr[k]();
}
//Here I expected to output: function 0, but instead outputs: function 3 WTF!
arr[0] ();
That produces the ddesire result.
Here, you define an anonymous function:
(function(index){ ... }(i))
that is immediately invoked with i as a parameter. That parameter is referred to as index in the function body (not that it's important, even if you still called it i - it would work, since the "inner" i would shadow the "outer" one).
That function returns a function that has a different closure - on index, which is not important since index is not avialable after the immediately invoked function exists.
Another way would be using some sort of iterator - map, where supported, would do OK.
It's a common problem, after you've read the duplicate posted in the comments, here's a possible solution:
var arr = [0,1,2].map(function(i){
return function(){
console.log("function " + i);
};
});
for(var i = 0; i < 3; i++) {
arr[i]();
}
arr[0]();
By creating an isolate scope with map we avoid the problem altogether. Using underscore's _.range you could replace your loop patterns with:
_.range(0,10).map(function(index){
...
})
The question linked to in one of the comments will give you the general answer.
But I'll also specifically address what you're seeing here, because it might still be slightly confusing even after understanding the answers to the other question.
The main thing to realize here is that there is only one i variable. Even though it looks like you're declaring it twice with the var keyword, the second "declaration" is essentially ignored and treated like any ordinary assignment. You see, the for keyword does not introduce a new scope in JavaScript. So these two snippets are equivalent:
for (var i = 0; i < 3; i++) {}
And:
var i;
for (i = 0; i < 3; i++) {}
Once you realize that, and you get that the functions you create in the first loop all close over the same i, then you can understand why the second loop appears to be "correct" by your intuition: at the start of the loop you set i to 0, and then after each call you increment it. So even though all of them close over the same i, you're changing its value between calls!
And of course, for that last call, the value of i is still 3 since that's what it was at the end of the second loop and you didn't change it from that.

JavaScript addEventListener() not working as expected

I have never used addEventListener(), but I cannot write the HTML equivalent I would like for each <div> I am treating as a button because of the way I am generating content. The equivalent would be:
<div onmousedown="jsItems[someId].toggleImage(someGallery, someIndex);"></div>
What I've been trying is this:
JsTree.prototype.addGalleries = function(inElements) {
// ...unrelated code here removed for StackOverflow...
for (var i = 0; i < this.jsGalleries.length; i++) {
for (var j = 0; j < this.jsGalleries[i].buttons.length; j++) {
var self = this;
this.jsGalleries[i].buttons[j].addEventListener("mousedown", function() {
self.toggleImage(i, j);
});
}
}
}
Where i counts from 0 to 1 and j counts from 0 to 2 (for both i in this case), i represents someGallery, j represents someIndex, and I could access someId with this.id inside the code above (or with self.id inside addEventListener's function).
The problem is that although clicking on one of these "buttons" (<div>s) does trigger:
JsTree.prototype.toggleImage = function(inGallery, inIndex) {
alert(this.id+", "+inGallery+", "+inIndex);
}
that it always alerts "8, 2, 3" regardless of which button is clicked. The "8" is correct but I have no idea why "2" or "3" are alerted. They seem to just be 1 more than what i and j count to (verified by trying j < this.jsGalleries[i].buttons.length-1 which alerts "8, 2, 2").
Edit: someId, someGallery, and someIndex are not real variables, they are junk I made up to try to explain the problem.
This is a classic JS mistake. The problem is that the values of i and j are not captured in any function scope, and your event handlers are asynchronous. That means that when your event handler runs, both for loops have run to completion, thus i == this.jsGalleries.length and j === this.jsGalleries[this.jsGalleries.length - 1].buttons.length.
Try out one of these:
JsTree.prototype.addGalleries = function(inElements) {
// ...unrelated code here removed for StackOverflow...
for (var i = 0; i < this.jsGalleries.length; i++) {
for (var j = 0; j < this.jsGalleries[i].buttons.length; j++) {
(function(self, innerI, innerJ){
var galleryEl = self.jsGalleries[innerI].buttons[innerJ];
galleryEl.addEventListener("mousedown", function() {
self.toggleImage(innerI, innerJ);
});
})(this, i, j);
}
}
}
Or possibly clearer:
JsTree.prototype.addGalleries = function(inElements) {
// ...unrelated code here removed for StackOverflow...
var addHandler = function(self, i, j){
self.jsGalleries[i].buttons[j].addEventListener("mousedown", function() {
self.toggleImage(i, j);
});
};
for (var i = 0; i < this.jsGalleries.length; i++) {
for (var j = 0; j < this.jsGalleries[i].buttons.length; j++) {
addHandler(this, i, j);
}
}
}
It's not a problem with addEventListener. This is a common mistake. In order to understand what's going on, I have to explain how closures work.
When you have a loop and a function inside of it:
var i = 5;
while(i--){
setTimeout(function(){
console.log(i);
}, 100);
}
Each function is given a reference to the variable i. That means that they don't retain the value of i at the time you defined them. Again, I'll restate, each function has a reference to the same variable i, not to the value that it had at the time the function was declared. In my example above, all of the setTimeout's are defined asynchronously. The anonymous functions all fire at 100 milliseconds and each one logs the value that's in i at the time that the function was run. In my example, that value would be -1 for all the functions.
There are 2 ways to solve this. I'll show you the easy one first:
for (var i = 0; i < this.jsGalleries.length; i++) {
for (var j = 0; j < this.jsGalleries[i].buttons.length; j++) {
var self = this;
self.gallery = {i: i, j: j};
this.jsGalleries[i].buttons[j].addEventListener("mousedown", function() {
self.toggleImage(self.gallery.i, self.gallery.j);
});
}
}
Here, you're storing the values on the actual DOM element. These values are equivalent to the values at the time that the loop was run, so the event listener grabs the correct value. Notice I nested the value in an object called gallery. I did this to kind of namespace it. It's not a good idea to store values on elements in the DOM, just in case browsers end up implementing a property with the same name. I feel like gallery is safe enough.
The other option, and probably the best practice, for fixing this is to use closures to your advantage.
for (var i = 0; i < this.jsGalleries.length; i++) {
for (var j = 0; j < this.jsGalleries[i].buttons.length; j++) {
var self = this;
this.jsGalleries[i].buttons[j].addEventListener("mousedown", (function closure(self, i, j){
return function actualListener(){
self.toggleImage(i, j);
}
})(self, i, j));
}
}
In this case, we create a self executing function (called closure in my example) which runs immediately when we're creating the listener. Let me state it again, this function runs the moment the listener is being added, NOT when it's run. The reason we do this is so we can pass in the values we want to save for later, in this case, self, i, and j. Then, when the event occurs, the function that ACTUALLY gets run is the inner function (called actualListener). actualListener has a copy of all the values stored in its closure at the time that the closure function was run.

Why aren't these two bits of JavaScript equivalent?

in jquery 1.4.2, ff 3.6.6:
The following code produces three divs, which write messages to the firebug console as you would expect. However, if you uncomment out the loop and comment out the 3 lines doing it manually, it doesn't work - mousing over any of the divs results in "three" being written to the console.
Why are these two methods any different than each other? In each one you use a selector to find the element and add an event to it.
<head>
<script type="text/javascript" src="/media/js/jquery.js"></script>
<script>
$( document ).ready( function() {
$("#one").mouseenter(function(){console.log("one")})
$("#two").mouseenter(function(){console.log("two")})
$("#three").mouseenter(function(){console.log("three")})
// names=['one','two','three'];
// for (k in names){
// id=names[k]
// $("#"+id).mouseenter(function(){console.log(id)})
// }
})
</script>
</head>
<body>
<span id="one">ONE</span>
<p><span id="two">TWO</span></p>
<p><span id="three">THREE</span></p>
</body>
You would be having a very common closure problem in the for in loop.
Variables enclosed in a closure share the same single environment, so by the time the mouseenter callback is called, the loop will have run its course and the id variable will be left pointing to the value of the last element of the names array.
This can be quite a tricky topic, if you are not familiar with how closures work. You may want to check out the following article for a brief introduction:
Mozilla Dev Center: Working with Closures
You could solve this with even more closures, using a function factory:
function makeMouseEnterCallback (id) {
return function() {
console.log(id);
};
}
// ...
var id, k,
names = ['one','two','three'];
for (k = 0; k < names.length; k++) {
id = names[k];
$("#" + id).mouseenter(makeMouseEnterCallback(id));
}
You could also inline the above function factory as follows:
var id, k,
names = ['one','two','three'];
for (k = 0; k < names.length; k++) {
id = names[k];
$("#" + id).mouseenter((function (p_id) {
return function() {
console.log(p_id);
};
})(id));
}
Any yet another solution could be as #d.m suggested in another answer, enclosing each iteration in its own scope:
var k,
names = ['one','two','three'];
for (k = 0; k < names.length; k++) {
(function() {
var id = names[k];
$("#" + id).mouseenter(function () { console.log(id) });
})();
}
Although not related to this problem, it is generally recommended to avoid using a for in loop to iterate over the items of an array, as #CMS pointed out in a comment below (Further reading). In addition, explicitly terminating your statements with a semicolon is also considered a good practice in JavaScript.
This happens because when the code of the anonymous function function(){console.log(id)} is executed, the value of id is three indeed.
To can use the following trick to enclose the value on each loop iteration into the anonymous callback scope:
names=['one', 'two', 'three'];
for (var k in names) {
(function() {
var id = names[k];
$("#"+id).mouseenter(function(){ console.log(id) });
})();
}

Categories