3

I have an array of objects created with a constructor. This is the constructor definition:

function Expander(elem, startingFlipped) {
  this.element = elem;
  this.flipped = startingFlipped;
}

(Below, rawElems is just a result of document.getElementsByClassName('foo');)

Once the DOMContentLoaded event fires, I then do this to find, construct, and push a new object into an array:

for (var i = 0; i < rawElems.length; i++) {
  expanders[i] = new Expander(rawElems[i], false);

  expanders[i].this.element.addEventListener("click", function (event) {
    console.log("Clicked " + expanders[i].this.element);
  });
}

The problem is, I don't know how the syntax for accessing the property for this. This currently throws the error:

Uncaught TypeError: Cannot read property 'element' of undefined at HTMLSpanElement.

Which is great in some ways, because these ARE <span> elements (console logging the object and the array shows it is properly populated).

But since I'm trying this constructor pattern, how do I properly reference the elements inside each object using this?

armadadrive
  • 963
  • 2
  • 11
  • 42

1 Answers1

6

The expression expanders[i].this.element means "look up the property on expanders whose name is the value of i, then on the result look up a property called this, and on the result of that, look up a property called element.

But your Expander object doesn't have a property on it called this. If you want to access the Expander object at expanders[i], you just use that, you don't add this. to it:

expanders[i].element.addEventListener("click", function (event) {
//          ^--- No this. here
    console.log("Clicked " + this);
    // Here, use `this` -----^
});

Within the event handler, this will refer to the DOM element that you hooked the event on, so you don't access it via your Expander at all.

If you need to access your expander in there, you need to give yourself a reference to it that will still be valid when the event occurs. You can't use expanders[i] for that with your current loop, because i's value will have changed before the event happens.

You have lots of options for this part, discussed in JavaScript closure inside loops – simple practical example. One of the simplest is to use Array.prototype.forEach instead of for:

Array.prototype.forEach.call(rawElems, function(element, index) {
    var expander = expanders[index] = new Expander(element, false);
    element.addEventListener("click", function (event) {
        // You can use `element` and `expander` here
        // If you liked, you could *not* have the `expander` variable
        // and use `expanders[index]` here, because unlike the `i` in
        // your `for` loop, the value `index` won't change.
    });
});

In an ES2015 (aka "ES6") environment or later, you could use let in your for loop: for (let i = 0; i < rawElems.length; ++i) and then expanders[i] would work correctly. (let is quite different from var even though they have similar purposes.)

Or you could use Function.prototype.bind to bind expanders[i] to the event handler function, then use this to refer to the expander, and this.element (or event.currentTarget) to refer to the element. That has the advantage that you can define the handler once and reuse it:

for (var i = 0; i < rawElems.length; i++) {
  expanders[i] = new Expander(rawElems[i], false);
  expanders[i].element.addaddEventListener("click", handleExpanderClick.bind(expanders[i]));
}
function handleExpanderClick(event) {
    // `this` = the expander
    // `this.element` = the element
    // `event.currentTarget` = the element (also)
}
Community
  • 1
  • 1
T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • Oh! Interesting. Could I also simply use a `.map()` method to assign the event handlers to each object.element in the array? – armadadrive Feb 12 '17 at 17:59
  • @armadadrive: Doh! Of course you could: `var expanders = Array.prototype.map.call(rawElems, function(element, index) { /* create, hook up, and return expander here */});` I should have thought of that. – T.J. Crowder Feb 12 '17 at 18:00
  • Hahaha, well I won't hold it against you. Thanks for your time and the well thought-out answer. – armadadrive Feb 12 '17 at 18:02