let keyword and closures? - javascript

I am learning about the new features in ES6. I have a question about let and it concerns this code:
for (let i = 0; i < 45; i++) {
var div = document.createElement('div');
div.onclick = function() {
alert("you clicked on a box #" + i);
};
document.getElementsByTagName('section')[0].appendChild(div);
}
I am confused by this code. What is happening with that div object that is declared at the start of each loop? Is that a brand new, separate object each time, somehow enclosed in the block scope of i? Or is this div object being overwritten each pass through the loop and if so, how does it maintain it's connection to the i it is given via let?

When I like to get a better understanding of what's happening in ES6 code, I enter my Javascript into the BabelJS REPL.
Your code when entered into the REPL outputs:
'use strict';
var _loop = function (i) {
div = document.createElement('div');
div.onclick = function () {
alert("you clicked on a box #" + i);
};
document.getElementsByTagName('section')[0].appendChild(div);
};
for (var i = 0; i < 45; i++) {
var div;
_loop(i);
};
Because you used let to assign i, its value is only available in the scope of the loop (or the function in the Babel example) for each loop iteration. To get the same functionality for the div variable, you could assign that variable in the loop body.
for (let div, i = 0; i < 45; i++) {
div = document.createElement('div');
...
}
Lastly, about closures and holding on to the i variable, you're one step away from creating a closure to maintain the current i value for each div.
// Create a function to hold on to a specific number
function createOnClick(index) {
return function() {
alert("you clicked on a box #", index);
};
};
// Assign the function to the element's action
div.onClick = createOnClick(i);
Without the function factory, the onClick value would always get the maximum i value of 44. This is because the function is being run after the entire loop has iterated and i has stopped at i < 45.

The <div> is a brand new object in each iteration, but it isn't enclosed in the block scope of i.
The function expression that is attached to the div is however also a brand new object, but this object is closing over i.

On each iteration div is a branch new object element.
Let defines i to be accessible only within foreach loop (What's the difference between using "let" and "var" to declare a variable?).
Once the div element is appended to "section" element and for loop starts a new iteration, you lose a reference to recently appended div by overriding it with a new one.

Related

Adding onlick event to multiple elements with params

I am dynamically creating a table of elements and storing them in an array. The following may seem like an absolute nightmare but this is how I have decided to sort it. My problem now comes to the addEventListener where I want to add an onclick event connected to PlayMusic(). I have tried a simple .onclick = and left out the function(){} but then the PlayMusic() gets executed immediately. Having the function(){} in there, when I click on one of these elements the first param (i) is the "last number used" (aka 22 out of 21 elements). How would I go about making sure each of these onclicks has the correct index in their params?
var thetable = document.getElementById("mustable");
for(var i=0; i<fullists.length-1; i++)
{
fullists[i][2] = [];
fullists[i][3] = [];
for(var j=0; j<fullists[i][1].length; j++)
{
var row = thetable.insertRow();
fullists[i][2][j] = row.insertCell();
fullists[i][2][j].className = "musentry";
var header = fullists[i][0].substring(0,fullists[i][0].lastIndexOf("."));
if(fullists[i][1][j][1] != undefined)
var title = fullists[i][1][j][1];
else
var title = fullists[i][1][j][0].substring(fullists[i][1][j][0].lastIndexOf("/"));
fullists[i][2][j].innerHTML = header + "<br /><b>" + title + "</b>";
fullists[i][2][j].addEventListener("click",function() { PlayMusic(i,j); },false);
fullists[i][3][j] = 0;
}
}
The issue is that by the time the function executes, i already has a different value because the loop already continued executing. If you change your loop to use let i instead of var i (same for j) it will work, because let in the for iterator variable has a special behavior where it actually creates another copy of the variable scoped to the inside of the loop on every iteration, so that copy won't change.
Another way, which is basically the same thing but done explicitly: Store it inside another block-scoped variable first. E.g. const i2 = i and then use i2 inside the function () {}. Same for j.
Alternatively, write .addEventListener(..., PlayMusic.bind(null, i, j)). With bind you can create a new function from a function, where a this and arguments are already bound to it. Since the binding happens immediately and thereby captures the current values of i and j, that solves it too.

How to add onclick functions in an array of buttons using a for loop properly?

I'm making a kind of HTML calculator to test something I have in mind.
I've used a for loop to create the buttons of the keypad. The display is a text field.
Then I used a for loop to add the functions in the buttons:
for (var i = 0; i < 10; i++)
{
buttons[i].onclick = function()
{
display.value += i;
};
}
What I was trying to do is to make, for example, buttons[0] add "0" to the value of the text field when clicked. Instead, clicking any button added "10" in the text field. Why? How can I make it right?
You almost got it right , you just need to change var to let in your loop declaration :
for (let i = 0; i < 10; i++)
{
buttons[i].onclick = function()
{
display.value += i;
};
}
What's the difference between using "let" and "var"? Here you can get more info about your issue.
Your problem is that you are referencing i directly in your functions that you are binding to your Buttons. i will actually continue to exist even after you bound all your events, and its value will be the last value of the iteration 10. So whenever a click function runs, it looks up i and finds the last value you set (10) and takes that value. What you want to do is add a constant reference instead - so that you bind that value you have during the loop and keep that reference forever, no matter how i might change later.
for (var i = 0; i < 3; i++) {
const localValue = i
buttons[i].onclick = function()
{
counter += localValue;
counterElement.innerHTML = counter
};
}
I created a small example fiddle here: https://jsfiddle.net/4k8cds9n/ if you run this you should see the buttons in action. Some related reading for this topic would be around scopes in javascript, one good article: https://scotch.io/tutorials/understanding-scope-in-javascript

How do I loop through an array of objects and fill the object in the mouseover function?

I'm a first year student I've been scrolling through Stack Overflow and have read a lot about the object problem (reference) but I can't figure out the solution to my problem.
I have made arrays of objects and looping over them to fill a div with all the info like img, name, value, so far no problem here.
The problem is with filling a mouseover function (attached to the image) with the object I'm looping through at the moment, so later when I hover over the image all the info of that particular object is shown on another div.
for (i = 0; i < arrgezelschap.lenght; i++) {
var x = arrgezelschap[i];
var element = document.createElement("img");
element.src = x.artikelFoto + "k.jpg";
element.addEventListener('mouseover', function() {
showinfo(x)
});
inhoud.append(element);
}
In the function showinfo(object) the output is always the last object of the array.
Why is this and what do I need to do so that it saves or points to the object that it's looping through at the moment in my function?
TL;DR: change var x to let x
I can't really do a better job explaining than Creating closures in loops: A common mistake, but I'll take a shot at rephrasing it.
Compare the output of these two snippets (below). The only difference is var vs let. The example demonstrates creating 5 functions in a loop, but does not call them yet. Each function references variables declared inside the loop, outside the loop and in the for itself. Then, at the end of the loop, we call all the functions to see what we got.
In the first case, the variables outside, i (the loop variable) and inside (declared inside the loop) are all declared with var. They are the same variable on every iteration of the loop. The inside var is hoisted to the top of the scope (outside the loop).
When we call all the functions we created, we will see that they all refer to the one-and-only instance of each variable, and they all have the value that the variables have after completion of the loop.
let functions = [];
var outside = 0;
for (var i = 0; i < 5; ++i) {
outside = i * 10;
var inside = i * 100;
functions.push(() => { console.log(outside, i, inside); })
}
functions.map(f => f()); // call all the functions
Output:
40 5 400
40 5 400
40 5 400
40 5 400
40 5 400
In this second example, the variables are all declared with let. The variable i declared in the for and the variable inside declared inside the body of the loop are different variables on each iteration of the loop. But the outside variable is declared outside the loop, so there's still only one outside variable that is used in every iteration of the loop.
When we call all the functions we made this time, we see that each function is displaying a different variable i and inside and their values are the value they held during that particular iteration of the loop, because the variables only existed for that iteration of the loop and the function was bound to the instance of the variable that was used for that iteration. But the outside variable is the same variable every iteration and holds only one value: the value that it has at the end of the loop.
let functions = [];
let outside = 0;
for (let i = 0; i < 5; ++i) {
outside = i * 10;
let inside = i * 100;
functions.push(() => { console.log(outside, i, inside); })
}
functions.map(f => f()); // call all the functions
Output:
40 0 0
40 1 100
40 2 200
40 3 300
40 4 400
In your case, each function binds to the same (one and only) variable x. If you change your declaration of x from var x to let x then you will get a different variable x for each iteration of the loop, and the event listener function will be bound to a different x each time, which will have the value corresponding to that iteration of the loop.
Footnote: Hopefully functions.map(f => f()); is not confusing for you. It just calls all the functions in the array. It is the same as this:
for (var index = 0; index < functions.length; ++index) {
functions[index]();
}
This is because x is a reference here, not a value and it change while you loop. Have a look at this :
let x = 0;
let fcn = a => console.log(a);
function execAnotherFcn(fcn) {
fcn(x);
}
execAnotherFcn(fcn);
x++;
execAnotherFcn(fcn);
You could use the dataset attribute to store your information.
Here's my implementation:
const root = document.querySelector('#root');
function createImagePlaceholder(color, data) {
const el = document.createElement('div');
el.style.width = '50px';
el.style.height = '50px';
el.style.margin = '5px';
el.style.backgroundColor = color;
el.dataset = data;
root.appendChild(el);
el.addEventListener('mouseover', () => {
document.querySelector('pre').innerText = JSON.stringify(data);
});
el.addEventListener('mouseleave', () => {
document.querySelector('pre').innerText = '';
});
}
createImagePlaceholder('red', { text: 'I am a red block' });
createImagePlaceholder('blue', { text: 'I am a blue block' });
<div id="root"></div>
<pre><pre>
You can fix this by making the scope of element block level.
This happens because here the value of x is send as a closure and the var is defined as function level.The event listner function will get executed at a future time(not to the main thread), so at that time the value of x is changed by the loop to the last value.
This can be done using the let key word or using a IIFE.
1.
for (i = 0; i < arrgezelschap.length; i++) {
let x = arrgezelschap[i];
let element = document.createElement("img");
element.src = x.artikelFoto + "k.jpg";
element.addEventListener('mouseover', function() {
showinfo(x)
});
inhoud.append(element);
}
2.
for (i = 0; i < arrgezelschap.lenght; i++) {
var x = arrgezelschap[i];
var element = document.createElement("img");
element.src = x.artikelFoto + "k.jpg";
(function(x){element.addEventListener('mouseover', function() {
showinfo(x)
});})(x);
inhoud.append(element);
}
#PopHips answer explains the theory of what is going wrong. so here is a working example with your code so you can follow it.
for(i =0;i<arrgezelschap.lenght;i++){
var x = arrgezelschap[i];
var element = document.createElement("img");
element.src = x.artikelFoto + "k.jpg";
element.dataset.identifyer = i;
element.addEventListener('mouseover', function(e) {
showinfo(arrgezelschap[e.target.dataset.identifyer])
});
inhoud.append(element);
}
So because we're using an event listener it will give the first param as an EventArgs object, this contains a property called target that is the HTMLElement effected. we can use the dataset (data-) system to save the identifier to the object's dataset so we can use it in the event handler.
Please note this answer should not be used as it is, there is some really bad practice in this answer, NEVER CREATE A FUNCTION INSIDE A LOOP in production code.

Javascript returns negative number for index

Getting the negative value when i perform the onclick function in javascript
function sun()
{
var d,i;
var t = document.getElementById("table");
var rows = t.getElementsByTagName("tr");
for (i = 0; i < rows.length; i++) {
console.log("inside............." + i);
rows[i].onclick = function() {
d = (this.rowIndex);
console.log(d);
};
}
}
Though I'm not sure it's what's causing the exact issue you're noticing, you've encountered a pretty common JavaScript pitfall here by using a closure (anonymous function) inside of a loop. JavaScript, like many other languages that support functional programming, has the convenient property that functions can "close scope" around any variables visible to them at the time of their creation. So, as you've done there, you can use the value of d (or i) inside your function so long as it can see them when your function is declared.
Something funny happens inside a loop, though: every function you create within the loop shares the same scope, meaning they all share the exact same copies of d and i. As a result, when you click on any of your rows, the values of d and i used will be their values at the end of the loop, not the particular iteration you're targeting.
This is ordinarily fixed using something known as the "generator pattern," where you create a separate function that returns new functions closed over your desired scope. For example, in your code, you might do something like
function generateClickHandler(i, d) {
return function() {
d = (this.rowIndex);
console.log(d);
};
}
function sun()
{
var d,i;
var t = document.getElementById("table");
var rows = t.getElementsByTagName("tr");
for (i = 0; i < rows.length; i++) {
console.log("inside............." + i);
rows[i].onclick = generateClickHandler(i, d);
}
}
The new function generateClickHandler returns a function itself, but the important thing to notice here is that the returned function closes over the local arguments i and d, not the shared i and d values used in the loop — their values get copied when you call generateClickHandler. In this way, your code won't be subject to strange closure effects.

Changing function parameters for onkeypress event according to source id

I want to assign a function according to their id to all the input fields in a web page. To do this I wrote below code but all the input fields are running keyPress with same parameter..
:(
///////////////////Checks all available 'text' 'input's////////////////////
var inputs = document.getElementsByTagName('input');
var cnvtrInput = new Array();
for (var index = 0; index < inputs.length; index++) {
if (inputs[index].type == 'text') {
cnvtrInput[index] = new converter(inputs[index]);
inputs[index].onkeypress = function() {return keyPess(cnvtrInput[index])};
}
}
//index--;
With the last commented statement I found that the passing element of keyPress is the last value of index;
Finally I tried same with textareas but failed...
You are creating functions in a loop which is always tricky. After the loop finished, index will have the value inputs.length and your callback is referencing index. But it won't work either if you define a new variable in a loop, as JavaScript has no block scope, only function scope.
You have to capture the value in a new scope, e.g. by using an immediate function:
inputs[index].onkeypress = (function(value) {
return function() {return keyPess(value)};
}(cnvtrInput[index]));

Categories

Resources