This question already has answers here:
JavaScript closure inside loops – simple practical example
(44 answers)
Closed 6 years ago.
I have multiple canvas elements:
<canvas class="screen" width="250" height="250" data-json="{{json_encode($board)}}"></canvas>
In my javascript I do the following:
var canvas = document.getElementsByClassName('screen');
for (var i = 0; i < canvas.length; i++) {
var podium = new Image();
podium.canvas = canvas[i];
podium.onload = function() {
var coordinates = JSON.parse(podium.canvas.getAttribute('data-json');
console.log(coordinates);
//Later on I add more images or anything else based on coordinates.
}
podium.src = 'my_podium_image.jpg';
}
But coordinates always contains the data from the last <canvas> element in my HTML.
However if I put var coordinates = JSON.parse(podium.canvas.getAttribute('data-json'); outside onload and print the output, it prints the data-json attributes from every canvas which is correct, but in the load() function it's still logs only the last canvas' data attribute. (Multiple times, because of the for loop)
I have no idea what's going. :)
Your problem is about Javascript Clousure
How do JavaScript closures work?
when that function is executed, the value of podium is the last one that was assigned.
You should use a selfexecuted function that keeps the scope of podium
var canvas = document.getElementsByClassName('screen');
for (var i = 0; i < canvas.length; i++) {
(function(podium) {
podium.canvas = canvas[i];
podium.onload = function() {
var coordinates = JSON.parse(podium.canvas.getAttribute('data-json'));
console.log(coordinates);
//Later on I add more images or anything else based on coordinates.
}
podium.src = 'my_podium_image.jpg';
})(new Image())
}
var canvas = document.getElementsByClassName('screen');
for (var i = 0; i < canvas.length; i++) {
(function(index) {
var podium = new Image();
podium.canvas = canvas[i];
podium.onload = function() {
var coordinates = JSON.parse(podium.canvas.getAttribute('data-json');
console.log(coordinates);
//Later on I add more images or anything else based on coordinates.
podium.src = 'my_podium_image.jpg';
}(i));
}
That's because "onload" event is asynchronous when it got fired your loop already reached the end
Related
This question already has answers here:
JavaScript closure inside loops – simple practical example
(44 answers)
Closed 3 years ago.
I am curretly experiencing difficulties implementing an onclick event within a for loop. Instead of alerting the respective value it always returns undefined (presumably a scope problem, because the iteration itself works fine)
Until now I tried to pass on the i variable to the onclick function; however, with little success
for (var i = 0; i < timeSpanLength; i++) {
// creating the wrap for the month
var month = document.createElement("div");
month.className = 'month_element';
var reference_month = document.createElement("span");
reference_month.innerHTML = time_span[i];
//onclick event
reference_month.onclick = function(i) {
var month_beginning = signup_date;
var month_end = time_span[i];
alert(month_end);
//searchForData(month_beginning, month_end);
};
//append to container
month.appendChild(reference_month);
document.getElementById('time_container').appendChild(month);
}
The expected outcome is to trigger an alert which displays the same month which is displayed in the span element above. I need the variable to pass it on to another function.
Any help is highly appreciated since I am beginner in javascript.
for (var i = 0; i < timeSpanLength; i++) {
(function (index) {
// creating the wrap for the month
var month = document.createElement("div");
month.className = 'month_element';
var reference_month = document.createElement("span");
reference_month.innerHTML = time_span[index];
//onclick event
reference_month.onclick = function() {
var month_beginning = signup_date;
var month_end = time_span[index];
alert(month_end);
//searchForData(month_beginning, month_end);
};
//append to container
month.appendChild(reference_month);
document.getElementById('time_container').appendChild(month);
})(i);
}
This callback function handler is forming a closure with respect to the outer scope. Also var has a function scope, so in essence the block of code can be re-written as:
var i;
for (i = 0; i < timeSpanLength; i++) {
...
//onclick event
reference_month.onclick = function(i) {
var month_beginning = signup_date;
var month_end = time_span[i];
alert(month_end);
//searchForData(month_beginning, month_end);
};
...
}
So the var i is hoisted to the top and when the loop completes the value of i is timeSpanLength.length and this is what you use to access time_span[i] and that returns undefined.
Since with var the binding remains the same, the handlers registered will be referring the last value of i in the loop.
So you either need to use let in the for-loop:
for (let i = 0; i < timeSpanLength; i++) { ... }
Or an IIFE which forms a new scope bound to each new value of i from the loop:
for (var i = 0; i < timeSpanLength; i++) {
(function(i){
reference_month.onclick = function(i) {
var month_beginning = signup_date;
var month_end = time_span[i];
alert(month_end);
//searchForData(month_beginning, month_end);
};
})(i)
}
I thought making a simple function where if you click on a button a number will show up inside of a paragraph. And if you continue to click on the button the number inside the paragraph tag will increase. However, I'm getting an error message saying that getElementsByTagName is not a function. Here is the code on jsfiddle, I know there is something simple that I'm doing wrong but I don't know what it is.
HTML
<div class="resist" id="ex1"><h2>Sleep</h2><p></p><button>Resist</button></div>
<div class="resist" id="ex2"><h2>Eat</h2><p></p><button>Resist</button></div>
Javascript
var count = 0;
var resist = document.getElementsByClassName('resist') ;
for(var i = 0; i < resist.length; i++)
{ var a = resist[i];
a.querySelector('button').addEventListener('click', function(a){
count +=1;
a.getElementsByTagName('p')[0].innerHTML = count;
});
}
You are overwriting a variable with event object passed into event handler. Change the name to e maybe, or remove it altogether as you are not using it anyway:
a.querySelector('button').addEventListener('click', function(e /* <--- this guy */) {
count += 1;
a.getElementsByTagName('p')[0].innerHTML = count;
});
Another problem you are going to have is classical closure-in-loop issue. One of the solutions would be to use Array.prototype.forEach instead of for loop:
var count = 0;
var resist = Array.prototype.slice.call(document.getElementsByClassName('resist'));
// ES6: var resist = Array.from(document.getElementsByClassName('resist'));
resist.forEach(function(a) {
a.querySelector('button').addEventListener('click', function(e) {
count += 1;
a.getElementsByTagName('p')[0].innerHTML = count;
});
});
vars in Javascript are function scoped, so you must wrap your event listener binding in a closure function to ensure the variable you're trying to update is correctly set.
(Note: I've renamed a to div in the outer function and removed the arg from the inner click function).
var count = 0;
var resist = document.getElementsByClassName('resist') ;
var div;
for(var i = 0; i < resist.length; i++)
{
div = resist[i];
(function(div){
div.querySelector('button').addEventListener('click', function(){
count +=1;
div.getElementsByTagName('p')[0].innerHTML = count;
});
})(div);
}
This question already has answers here:
JavaScript closure inside loops – simple practical example
(44 answers)
Closed 7 years ago.
Why doesn't the code below print it's corresponding value when clicked?
Instead, it prints 5?
var recipeDiv = document.createElement("div");
var recipeUL = document.createElement("ul");
for (var i = 0; i < 5; ++i) {
var listNode = document.createElement("li");
listNode.innerHTML = i;
listNode.onclick = function() { alert(i); }
recipeUL.appendChild(listNode);
}
recipeDiv.appendChild(recipeUL);
addNodeToDOM(document.body, recipeDiv, 'recipe');
function addNodeToDOM(element, node, id) {
var div = document.createElement('div');
div.id = id;
div.appendChild(node);
element.appendChild(div);
}
I was able to reproduce the bug here: jsfiddle
But basically, I'm not sure if this is the convention for adding elements correctly to the DOM. If so, how come whenever I click on the list elements, it doesn't show
When, onclick handler executes, the value of i will always show 5 as the for loop ends at 5th iteration,
Use this.innerHTML,
listNode.onclick = function() { alert(this.innerHTML); }
Updated Fiddle
Another simple solution is to have a wrapper function.
for (var i = 0; i < 5; ++i) {
(function(i){
var listNode = document.createElement("li");
listNode.innerHTML = i;
listNode.onclick = function() { alert(i); }
recipeUL.appendChild(listNode);
})(i);
}
This way you can always rely on the iterator variable i.
This is where closure concept plays its role. Try modifying listNode.onclick = function() { alert(i); } line of code with this:
listNode.onclick = (function(index) { return function(){alert(index);} })(i);
I have updated your fiddle..
FIDDLE
This question already has answers here:
Javascript infamous Loop issue? [duplicate]
(5 answers)
JavaScript closure inside loops – simple practical example
(44 answers)
Closed 8 years ago.
I am making a game using JavaScript and HTML canvas. The 'onload' works perfectly without a for-loop. But 'onload' become buggy and unreasonable in a for-loop. However, I came up with a simple workaround to skip that problem. But I am still curious why. Here comes the codes and how is the workaround:
I have created an object:
function Monster() {
this.image = new Image();
this.ready = false;
}
and created to a few (let's say two) instances:
var monster = [];
for(var a = 0; a < 2; a++) {
monster.push(new Monster());
}
When I tried to initialize these objects, it's gonna fail:
for(n = 0; n < monster.length; n++) { //assume length is 2
monster[n].image.onload = function() {
monster[n].ready = true; /* Problem raise up here, n has the same value as monster.length.
If length is 2, this line gonna run 2 times */
};
monster[n].image.src = 'images/m1.png';
}
However, this problem could be solved easily by creating a function:
for(n = 0; n < monster.length; n++) {
makeMonster(n);
}
And:
var makeMonster = function(n) {
monster[n].image.onload = function() {
monster[n].ready = true;
};
monster[n].image.src = 'images/m1.png';
};
The question is, why?
The onload function is async, so by the time it fires the loop has completed and the value of n is whatever it was set to last.
You have to lock the value of the n variable in a new scope with a closure, creating a new function also creates such a scope, or like this
for(n = 0; n < monster.length; n++) {
(function(k) {
monster[k].image.onload = function() {
monster[k].ready = true;
}
monster[k].image.src = 'images/m1.png';
})(n);
}
I have been pulling my hair out trying to make this simple code work. It should render input fields in the given DOM, but it doesn't. Why not?
var elems = 10;
function generateElems() {
for (var i = 0; i < elems; i++) {
document.getElementsByTagName("div")[0].appendChild(document.createElement('input'));
}
//Clean up
var obj = null;
var elems = null;
}
generateElems();
Working DEMO
You are dealing with JavaScript variable hoisting here. Remove this line var elems = null; and your code should work.
It is considered best practise in JavaScript to declare all variables at the top of the function body.
Read this article for more information on JavaScript hoisting.
As we are discussing best practises, it's worth making a note that appending elements in loops is a bad idea for performance. You should use createDocumentFragment instead to append the elements to and then dump this to DOM. It saves expensive document reflows and makes significant difference in performance.
var elems = 10;
function generateElems() {
var d=document.createDocumentFragment();
for (var i = 0; i < elems; i++) {
d.appendChild(document.createElement('input'));
}
document.getElementsByTagName('div')[0].appendChild(d);
//Clean up
//var obj = null;
//var elems = null; ----> Commented out this line, it was causing the problem.
}
generateElems();
Don't set elms to null.
var elems = 10;
function generateElems() {
for (var i = 0; i < elems; i++) {
document.getElementsByTagName("div")[i].appendChild(document.createElement('input'));
}
}
generateElems();