web-dev-qa-db-fra.com

La valeur de "this" dans le gestionnaire utilisant addEventListener

J'ai créé un objet javascript via le prototypage. J'essaye de rendre une table dynamiquement. Bien que la partie de rendu soit simple et fonctionne correctement, je dois également gérer certains événements côté client pour le tableau rendu dynamiquement. Cela aussi est facile. J'ai des problèmes avec la référence "this" à l'intérieur de la fonction qui gère l'événement. Au lieu de "ceci" référence à l'objet, il fait référence à l'élément qui a déclenché l'événement.

Voir le code. La zone problématique est dans "ticketTable.prototype.handleCellClick = function ()" 

function ticketTable(ticks)
{
    // tickets is an array
    this.tickets = ticks;
} 

ticketTable.prototype.render = function(element)
    {
        var tbl = document.createElement("table");
        for ( var i = 0; i < this.tickets.length; i++ )
        {
            // create row and cells
            var row = document.createElement("tr");
            var cell1 = document.createElement("td");
            var cell2 = document.createElement("td");

            // add text to the cells
            cell1.appendChild(document.createTextNode(i));
            cell2.appendChild(document.createTextNode(this.tickets[i]));

            // handle clicks to the first cell.
            // FYI, this only works in FF, need a little more code for IE
            cell1.addEventListener("click", this.handleCellClick, false);

            // add cells to row
            row.appendChild(cell1);
            row.appendChild(cell2);


            // add row to table
            tbl.appendChild(row);            
        }

        // Add table to the page
        element.appendChild(tbl);
    }

    ticketTable.prototype.handleCellClick = function()
    {
        // PROBLEM!!!  in the context of this function, 
        // when used to handle an event, 
        // "this" is the element that triggered the event.

        // this works fine
        alert(this.innerHTML);

        // this does not.  I can't seem to figure out the syntax to access the array in the object.
        alert(this.tickets.length);
    }
49
Darthg8r

Vous devez "lier" le gestionnaire à votre instance.

var _this = this;
function onClickBound(e) {
  _this.handleCellClick.call(cell1, e || window.event);
}
if (cell1.addEventListener) {
  cell1.addEventListener("click", onClickBound, false);
}
else if (cell1.attachEvent) {
  cell1.attachEvent("onclick", onClickBound);
}

Notez que le gestionnaire d’événements normalise ici l’objet event (transmis comme premier argument) et appelle handleCellClick dans un contexte approprié (c’est-à-dire faisant référence à un élément qui était attaché à l’écouteur d’événements).

Notez également que la normalisation du contexte ici (c’est-à-dire définir this dans le gestionnaire d’événements) crée une référence circulaire entre la fonction utilisée comme gestionnaire d’événement (onClickBound) et un objet élément (cell1). Dans certaines versions de IE (6 et 7), cela peut entraîner et entraînera probablement une fuite de mémoire. Cette fuite est essentiellement le fait que le navigateur n'a pas libéré la mémoire lors de l'actualisation de la page en raison d'une référence circulaire existant entre l'objet natif et l'hôte. 

Pour le contourner, vous devez soit: a) abandonner la normalisation this; b) employer une stratégie de normalisation alternative (et plus complexe); c) "nettoyer" les écouteurs d’événements existants lors du déchargement de la page, c’est-à-dire en utilisant removeEventListener, detachEvent et des éléments nulling (ce qui rendrait malheureusement la navigation dans l'historique rapide du navigateur inutile).

Vous pouvez également trouver une bibliothèque JS qui s’occupe de cela. La plupart d’entre eux (par exemple: jQuery, Prototype.js, YUI, etc.) gèrent généralement les opérations de nettoyage comme décrit en (c).

39
kangax

Vous pouvez utiliser bind qui vous permet de spécifier la valeur à utiliser comme this pour tous les appels d'une fonction donnée.

   var Something = function(element) {
      this.name = 'Something Good';
      this.onclick1 = function(event) {
        console.log(this.name); // undefined, as this is the element
      };
      this.onclick2 = function(event) {
        console.log(this.name); // 'Something Good', as this is the binded Something object
      };
      element.addEventListener('click', this.onclick1, false);
      element.addEventListener('click', this.onclick2.bind(this), false); // Trick
    }

Un problème dans l'exemple ci-dessus est que vous ne pouvez pas supprimer l'écouteur avec bind. Une autre solution consiste à utiliser une fonction spéciale appelée handleEvent pour intercepter tous les événements:

var Something = function(element) {
  this.name = 'Something Good';
  this.handleEvent = function(event) {
    console.log(this.name); // 'Something Good', as this is the Something object
    switch(event.type) {
      case 'click':
        // some code here...
        break;
      case 'dblclick':
        // some code here...
        break;
    }
  };

  // Note that the listeners in this case are this, not this.handleEvent
  element.addEventListener('click', this, false);
  element.addEventListener('dblclick', this, false);

  // You can properly remove the listners
  element.removeEventListener('click', this, false);
  element.removeEventListener('dblclick', this, false);
}

Comme toujours mdn est le meilleur :). Je viens de copier la partie collée que de répondre à cette question.

59
gagarine

En outre, une autre méthode consiste à utiliser l'interface EventListener (à partir de DOM2! Vous vous demandez pourquoi personne ne l'a mentionnée, sachant que c'est le moyen le plus simple et conçu pour une telle situation.) 

Ie, au lieu de passer une fonction de rappel, vous transmettez un objet qui implémente l'interface EventListener. En termes simples, cela signifie simplement que vous devriez avoir une propriété dans l'objet appelée "handleEvent", qui pointe vers la fonction de gestionnaire d'événements. La principale différence ici est que, dans la fonction, this fera référence à l'objet transmis à addEventListener. C'est-à-dire que this.theTicketTable sera l'instance de l'objet dans le BelowCode. Pour comprendre ce que je veux dire, regardez attentivement le code modifié:

ticketTable.prototype.render = function(element) {
...
var self = this;

/*
 * Notice that Instead of a function, we pass an object. 
 * It has "handleEvent" property/key. You can add other
 * objects inside the object. The whole object will become
 * "this" when the function gets called. 
 */

cell1.addEventListener('click', {
                                 handleEvent:this.handleCellClick,                  
                                 theTicketTable:this
                                 }, false);
...
};

// note the "event" parameter added.
ticketTable.prototype.handleCellClick = function(event)
{ 

    /*
     * "this" does not always refer to the event target element. 
     * It is a bad practice to use 'this' to refer to event targets 
     * inside event handlers. Always use event.target or some property
     * from 'event' object passed as parameter by the DOM engine.
     */
    alert(event.target.innerHTML);

    // "this" now points to the object we passed to addEventListener. So:

    alert(this.theTicketTable.tickets.length);
}
11
kamathln

Je sais que ceci est un article plus ancien, mais vous pouvez aussi simplement assigner le contexte à une variable self, jeter votre fonction dans une fonction anonyme qui appelle votre fonction avec .call(self) et passe dans le contexte.

ticketTable.prototype.render = function(element) {
...
    var self = this;
    cell1.addEventListener('click', function(evt) { self.handleCellClick.call(self, evt) }, false);
...
};

Cela fonctionne mieux que la "réponse acceptée" car il n’est pas nécessaire d’attribuer au contexte une variable pour la classe entière ou globale, mais plutôt de le ranger dans la même méthode que celle utilisée pour l’événement.

5
Tom Doe

Fortement influencé par la réponse de Kamathln et Gagarine, je pensais pouvoir m'attaquer à ce problème.

Je pensais que vous pourriez probablement gagner un peu plus de liberté si vous mettiez handeCellClick dans une liste de rappel et utilisiez un objet utilisant l'interface EventListener sur l'événement pour déclencher les méthodes de liste de rappel avec le correct.

function ticketTable(ticks)
    {
        // tickets is an array
        this.tickets = ticks;
        // the callback array of methods to be run when
        // event is triggered
        this._callbacks = {handleCellClick:[this._handleCellClick]};
        // assigned eventListenerInterface to one of this
        // objects properties
        this.handleCellClick = new eventListenerInterface(this,'handleCellClick');
    } 

//set when eventListenerInterface is instantiated
function eventListenerInterface(parent, callback_type) 
    {
        this.parent = parent;
        this.callback_type = callback_type;
    }

//run when event is triggered
eventListenerInterface.prototype.handleEvent(evt)
    {
        for ( var i = 0; i < this.parent._callbacks[this.callback_type].length; i++ ) {
            //run the callback method here, with this.parent as
            //this and evt as the first argument to the method
            this.parent._callbacks[this.callback_type][i].call(this.parent, evt);
        }
    }

ticketTable.prototype.render = function(element)
    {
       /* your code*/ 
        {
            /* your code*/

            //the way the event is attached looks the same
            cell1.addEventListener("click", this.handleCellClick, false);

            /* your code*/     
        }
        /* your code*/  
    }

//handleCellClick renamed to _handleCellClick
//and added evt attribute
ticketTable.prototype._handleCellClick = function(evt)
    {
        // this shouldn't work
        alert(this.innerHTML);
        // this however might work
        alert(evt.target.innerHTML);

        // this should work
        alert(this.tickets.length);
    }
1
niall.campbell

Cette syntaxe de flèche fonctionne pour moi:

document.addEventListener('click', (event) => {
  // do stuff with event
  // do stuff with this 
});

this sera le contexte parent et non le contexte document

1
Mark

Qu'en est-il de

...
    cell1.addEventListener("click", this.handleCellClick.bind(this));
...

ticketTable.prototype.handleCellClick = function(e)
    {
        alert(e.currentTarget.innerHTML);
        alert(this.tickets.length);
    }

e.currentTarget pointe vers la cible qui est liée à "l'événement click" (à l'élément qui a déclenché l'événement) tant que

bind (this) conserve la valeur outerscope de this dans la fonction d'événement click.

Si vous souhaitez obtenir une cible exacte sur laquelle vous avez cliqué, utilisez plutôt e.target .

0
DevWL

Avec ES6, vous pouvez utiliser une fonction de flèche car elle utilisera la portée lexicale [0], ce qui vous évite d’avoir à utiliser bind ou self = this:

var something = function(element) {
  this.name = 'Something Good';
  this.onclick1 = function(event) {
    console.log(this.name); // 'Something Good'
  };
  element.addEventListener('click', () => this.onclick1());
}

[0] https://medium.freecodecamp.org/learn-es6-the-Dope-way-part-ii-arrow-functions-and-the-this-keyword-381ac7a32881

0
Chris