Der Wert von "this" im Handler mit addEventListener

77

Ich habe ein Javascript-Objekt per Prototyping erstellt. Ich versuche, eine Tabelle dynamisch zu rendern. Während der Rendering-Teil einfach ist und gut funktioniert, muss ich auch bestimmte clientseitige Ereignisse für die dynamisch gerenderte Tabelle behandeln. Das ist auch einfach. Ich habe Probleme mit der Referenz "this" innerhalb der Funktion, die das Ereignis behandelt. Anstatt "dies" auf das Objekt zu verweisen, verweist es auf das Element, das das Ereignis ausgelöst hat.

Siehe Code. Der Problembereich liegt in 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);
    }
Darthg8r
quelle

Antworten:

46

Sie müssen den Handler an Ihre Instanz "binden".

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);
}

Beachten Sie, dass der Ereignishandler hier das eventObjekt normalisiert (als erstes Argument übergeben) und handleCellClickin einem geeigneten Kontext aufruft (dh auf ein Element verweist, an das der Ereignis-Listener angehängt wurde).

Beachten Sie auch, dass die Kontextnormalisierung hier (dh die richtige Einstellung thisim Ereignishandler) einen Zirkelverweis zwischen der als Ereignishandler ( onClickBound) verwendeten Funktion und einem Elementobjekt ( cell1) erstellt. In einigen Versionen von IE (6 und 7) kann und wird dies wahrscheinlich zu einem Speicherverlust führen. Dieses Leck ist im Wesentlichen darauf zurückzuführen, dass der Browser bei der Seitenaktualisierung keinen Speicher freigibt, da zwischen dem nativen und dem Host-Objekt ein Zirkelverweis besteht.

Um dies zu umgehen, müssten Sie entweder a) die thisNormalisierung fallen lassen ; b) alternative (und komplexere) Normalisierungsstrategien anwenden; c) „aufzuräumen“ bestehende Ereignis - Listener auf Seite entladen, dh durch die Verwendung removeEventListener, detachEventund Elemente nulling (die leider machen würden Browser schnell Geschichte Navigation nutzlos).

Sie könnten auch eine JS-Bibliothek finden, die sich darum kümmert. Die meisten von ihnen (z. B. jQuery, Prototype.js, YUI usw.) führen normalerweise Bereinigungen durch, wie in (c) beschrieben.

Kangax
quelle
Woher kommt var _this = this; im Kontext meines Codes gehen? Muss ich dem Prototyp onClickBound (e) hinzufügen?
Darthg8r
In render, kurz vor dem Anhängen des Ereignis-Listeners. Sie können so ziemlich einfach die addEventListenerZeile aus dem Originalbeispiel durch dieses Snippet ersetzen .
Kangax
Es ist interessant, dass Sie Aufräumen erwähnen. Ich werde diese Objekte auch irgendwann im Prozess zerstören. Ich hatte ursprünglich geplant, nur .innerHTML = "" zu machen; Ich vermute, dass das in diesem Zusammenhang schlecht ist. Wie würde ich diese Tabellen zerstören und das erwähnte Leck vermeiden?
Darthg8r
Wie ich bereits sagte, schauen Sie in removeEventListener/ detachEventund brechen Sie Zirkelverweise. Hier ist eine gute Erklärung für ein Leck - jibbering.com/faq/faq_notes/closures.html#clMem
kangax
4
Ich weiß nicht warum, aber dieses Selbst = diese Tricks scheinen mir immer falsch zu sein.
Gagarine
84

Sie können verwenden binden , die Sie den Wert festlegen können , die als verwendet werden soll dies für alle Anrufe auf eine bestimmte Funktion.

   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
    }

Ein Problem im obigen Beispiel ist, dass Sie den Listener nicht mit bind entfernen können. Eine andere Lösung ist die Verwendung einer speziellen Funktion namens handleEvent , um Ereignisse abzufangen :

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);
}

Wie immer ist mdn das Beste :). Ich habe nur den Teil kopiert und dann diese Frage beantwortet.

gagarine
quelle
10

Eine weitere Möglichkeit ist die Verwendung des EventListener-Interfaces (von DOM2 !! Ich frage mich, warum niemand es erwähnt hat, da es der sauberste Weg ist und für eine solche Situation gedacht ist.)

Das heißt, anstatt eine Rückruffunktion zu übergeben, übergeben Sie ein Objekt, das die EventListener-Schnittstelle implementiert. Einfach ausgedrückt bedeutet dies nur, dass Sie im Objekt eine Eigenschaft namens "handleEvent" haben sollten, die auf die Ereignishandlerfunktion verweist. Der Hauptunterschied besteht darin, dass innerhalb der Funktion thisauf das an das übergebene Objekt verwiesen wird addEventListener. Das heißt, this.theTicketTablewird die Objektinstanz im folgenden Code sein. Um zu verstehen, was ich meine, schauen Sie sich den geänderten Code genau an:

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);
}
kamathln
quelle
Ein bisschen ordentlich, aber anscheinend kannst du das nicht removeEventListener()?
Knutole
3
@knutole, ja du kannst. Speichern Sie einfach das Objekt in einer Variablen und übergeben Sie die Variable an addEventListener. Sie können hier als Referenz suchen: stackoverflow.com/a/15819593/2816199 .
Tomkwi
TypeScript scheint diese Syntax nicht zu mögen, daher wird kein Aufruf addEventListenermit einem Objekt als Rückruf kompiliert . Teufel noch mal.
David R Tribble
@DavidRTribble Haben Sie versucht, das Objekt einer Variablen zuzuweisen und dann die Variable zu übergeben? Ich habe TypeScript noch nicht persönlich ausprobiert, aber wenn es sich genau um ein Syntaxproblem handelt und nicht
darum,
6

Mit ES6, können Sie einen Pfeil Funktion wie die lexikalischen Scoping verwenden [0], ermöglicht es Ihnen , zu vermeiden , zu verwenden bindoder 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

Chris
quelle
6

Diese Pfeilsyntax funktioniert bei mir:

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

dies wird die seine Eltern Kontext und nicht das Dokument Kontext

Kennzeichen
quelle
5

Ich weiß, dass dies ein älterer Beitrag ist, aber Sie können den Kontext auch einfach einer Variablen zuweisen self, Ihre Funktion in eine anonyme Funktion werfen, die Ihre Funktion mit aufruft .call(self)und den Kontext weitergibt.

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

Dies funktioniert besser als die "akzeptierte Antwort", da dem Kontext keine Variable für die gesamte Klasse oder global zugewiesen werden muss, sondern ordentlich in derselben Methode versteckt ist, die auf das Ereignis wartet.

Tom Doe
quelle
2
Mit ES5 erhalten Sie das gleiche mit nur : cell1.addEventListener('click', this.handleCellClick.bind(this));. Lassen Sie das letzte Argument, falsewenn Sie Kompatibilität mit FF <= 5 wünschen.
Tomékwi
Das Problem dabei ist, dass Sie, wenn Sie andere Funktionen haben, die vom Handler aufgerufen werden, sich selbst weitergeben müssen.
mkey
Dies ist die Lösung, die ich verwendet habe. Ich hätte es vorgezogen, die .bind()Methode zu verwenden, musste sie aber .call()stattdessen verwenden, da unsere App IE8 noch eine Weile unterstützen muss.
David R Tribble
1
Früher habe ich verwendet self, bis ich selfdas gleiche gefunden habe window, jetzt benutze ich me.
Qian Chen
1
Wie benutzt du removeEventListenerdiesen Weg?
Tamb
1

Stark beeinflusst von Kamathln und Gagarines Antwort dachte ich, ich könnte das angehen.

Ich dachte, Sie könnten wahrscheinlich ein bisschen mehr Freiheit gewinnen, wenn Sie handeCellClick in eine Rückrufliste einfügen und ein Objekt verwenden, das die EventListener-Schnittstelle für das Ereignis verwendet, um die Rückruflistenmethoden mit dem richtigen Wert auszulösen.

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);
    }
niall.campbell
quelle
0

Wie wäre es mit

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

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

e.currentTarget zeigt auf das Ziel, das an das " Klickereignis " (an das Element, das das Ereignis ausgelöst hat) gebunden ist

bind (this) behält den Outerscope-Wert thisinnerhalb der Click-Event-Funktion bei.

Wenn Sie ein genaues Ziel anklicken möchten, verwenden Sie stattdessen e.target .

DevWL
quelle