4

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?

1.Stijn
  • 57
  • 5
  • Does this have something to do with the binding of x? Is this in a loop? Because there is only one x in your example, and if it's in a loop then its value might change. So if there is a loop, please show your loop. – Wyck May 23 '19 at 14:51
  • Possible duplicate of [addEventListener using for loop and passing values](https://stackoverflow.com/questions/19586137/addeventlistener-using-for-loop-and-passing-values) – karthick May 23 '19 at 14:52
  • When you say *"array of objects"*, do you mean an array of object literals? En waar is de `inhoud`? – Malekai May 23 '19 at 15:21
  • 1
    @LogicalBranch read the code and you would see it's an array of logical objects E.G `[{artikelFoto:"test"}]` – Barkermn01 May 23 '19 at 15:22
  • https://stackoverflow.com/questions/750486/javascript-closure-inside-loops-simple-practical-example – Jamiec May 23 '19 at 15:25
  • 1
    I would like to add that adding an event listener with the same code in it in a loop is the horrific practice you're creating an instruction set every loop that does exactly the same thing you should not you should have the function defined outside the loop and just attached to the item this also helps with preventing these in loop collisions, with decent editors (they will be complaining about use of unknown variable) – Barkermn01 May 23 '19 at 15:52

5 Answers5

2

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]();
}
Wyck
  • 10,311
  • 6
  • 39
  • 60
1

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);
PopHip
  • 700
  • 6
  • 23
  • 1
    What you have described here (lexical scope and closures) is also described at this page: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures The solution for how to get a different instance of `x` on each iteration of the loop is also described on that page. Specifically: [Creating closures in loops: A common mistake](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures#Creating_closures_in_loops_A_common_mistake) – Wyck May 23 '19 at 14:59
  • thx for the fast reply @Wyck ! so if i understand it correctly i can not use the same variable name in a loop for objects i need to make x unique in every part of the loop? – 1.Stijn May 23 '19 at 15:21
  • @1.Stijn I added my own answer that hopefully explains fully. – Wyck May 23 '19 at 17:44
0

@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.

Barkermn01
  • 6,781
  • 33
  • 83
  • ok thx for the reply! very helpful! so using the same `var element` to put in other elements in my `div` is not an option? just keep that one for the image and make a new one each time i want a new element in my loop and append them all outside the loop? – 1.Stijn May 23 '19 at 16:44
0

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>
Paul Cosma
  • 303
  • 2
  • 11
0

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);
}
Vishnu
  • 897
  • 6
  • 13
  • This is weird to me. I think in section 1 you meant to put the `let` on the `x`, but you put it on the `element`. ??? – Wyck May 23 '19 at 17:08