2009-08-27 5 views
41

Ho creato un oggetto javascript tramite la prototipazione. Sto cercando di rendere una tabella dinamicamente. Mentre la parte di rendering è semplice e funziona bene, devo anche gestire alcuni eventi lato client per la tabella resa dinamicamente. Anche questo è facile. Dove sto avendo problemi è con il "questo" riferimento all'interno della funzione che gestisce l'evento. Invece di "questo" fa riferimento all'oggetto, fa riferimento all'elemento che ha generato l'evento.Il valore di "this" nel gestore utilizzando addEventListener

Vedere codice. L'area problematica è 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); 
    } 

risposta

31

hai bisogno di "legare" gestore per l'istanza.

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

noti che gestore eventi qui normalizza event oggetto (passato come primo argomento) e richiama handleCellClick in un contesto appropriato (cioè riferito ad un elemento che è stato attaccato ascoltatore evento).

noti inoltre che la normalizzazione contesto here (cioè modificando corretta this in gestore di eventi) crea un riferimento circolare tra funzione utilizzata come gestore (onClickBound) e un oggetto elemento (cell1). In alcune versioni di IE (6 e 7) questo può, e probabilmente lo farà, provocare una perdita di memoria. Questa perdita, in sostanza, è che il browser non riesce a rilasciare memoria all'aggiornamento della pagina a causa del riferimento circolare esistente tra l'oggetto nativo e l'oggetto host.

Per aggirare il problema, è necessario a) annullare la normalizzazione this; b) utilizzare una strategia di normalizzazione alternativa (e più complessa); c) "ripulire" i listener di eventi esistenti allo scaricamento della pagina, utilizzando removeEventListener, detachEvent e gli elementi null (che purtroppo renderebbe inutilizzabile la navigazione rapida nella cronologia dei browser).

È inoltre possibile trovare una libreria JS che si occupa di questo. La maggior parte di essi (ad esempio: jQuery, Prototype.js, YUI, ecc.) Di solito gestiscono le operazioni di pulizia come descritto in (c).

+0

Dove var = questo = questo; nel contesto del mio codice vai? Devo aggiungere onClickBound (e) al prototipo? – Darthg8r

+0

In 'render', subito prima di collegare il listener di eventi. Puoi praticamente sostituire la riga 'addEventListener' dall'esempio originale con questo snippet. – kangax

+0

È interessante che tu menzioni la pulizia. In effetti, distruggerò questi oggetti anche a un certo punto del processo. Inizialmente avevo programmato di fare .innerHTML = ""; La mia ipotesi è che sia male in questo contesto. Come distruggerei questi tavoli ed evitare la perdita menzionata? – Darthg8r

5

So che questo è un post più vecchio, ma puoi anche semplicemente assegnare il contesto a una variabile self, lanciare la tua funzione in una funzione anonima che richiama la tua funzione con .call(self) e passa nel contesto.

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

Questo funziona meglio che la "risposta accettata" perché il contesto non ha bisogno di assegnare una variabile per l'intera classe o globale, piuttosto è ben nascosto all'interno dello stesso metodo che intercetta l'evento.

+1

Con ES5 si ottiene lo stesso usando semplicemente: 'cell1.addEventListener ('click', this.handleCellClick.bind (this));'. Lascia l'ultimo argomento 'false' se vuoi la compatibilità con FF <= 5. – tomekwi

+0

Tranne 'bind' non funziona in PhantomJS senza polyfill. –

+0

Il problema è che se si hanno altre funzioni chiamate dal gestore, è necessario passare automaticamente la linea. – mkey

9

Inoltre, un altro modo è quello di utilizzare il EventListener Interface (da DOM2 !! Domandandosi perchè nessuno ha menzionato che, considerando che è il modo più pulita e significava per una simile situazione.)

Cioè, invece di un passando una funzione di callback, si passa un oggetto che implementa l'interfaccia EventListener. In poche parole, significa semplicemente che dovresti avere una proprietà nell'oggetto chiamato "handleEvent", che punta alla funzione del gestore di eventi. La differenza principale qui è che all'interno della funzione, this si riferirà all'oggetto passato a addEventListener. Cioè, this.theTicketTable sarà l'istanza dell'oggetto nel codice sottostante.Per capire cosa intendo, guardare il codice modificato con attenzione:

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

Un po 'pulito, ma sembra che non puoi 'removeEventListener()' con questo? – knutole

+3

@knutole, sì, puoi. Basta salvare l'oggetto in una variabile e passare la variabile a 'addEventListener'. Puoi guardare qui per riferimento: http://stackoverflow.com/a/15819593/2816199. – tomekwi

+0

La sintassi non sembra gradita da TypeScript, quindi non compila una chiamata a 'addEventListener' con un oggetto come callback. Dannazione. –

44

È possibile utilizzare bind che consente di specificare il valore che dovrebbe essere utilizzato come questo per tutte le chiamate a una data funzione.

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 problema nell'esempio precedente è che non è possibile rimuovere l'ascoltatore con bind. Un'altra soluzione sta usando una funzione speciale chiamata handleEvent per catturare tutti gli eventi:

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

Come sempre mdn è il migliore :). Ho appena incollato la parte che rispondo a questa domanda.

1

Fortemente influenzato dalla risposta di kamathln e gagarine, pensavo di poterlo affrontare.

Stavo pensando che si potrebbe ottenere un po 'più di libertà se si inserisce handeCellClick in un elenco di callback e si utilizza un oggetto utilizzando l'interfaccia EventListener sull'evento per attivare i metodi dell'elenco di callback con il corretto.

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

Che dire

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

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

e.currnetTarget punti al bersaglio che si legano con il "click evento" (per l'elemento che ha generato l'evento), mentre

bind (this) preservare il valore di "this" di un outescope all'interno della funzione di evento click.

se si desidera ottenere esattamente il target su cui si fa clic, utilizzare invece e.target.