O valor de "this" dentro do manipulador usando addEventListener
-
20-09-2019 - |
Pergunta
Eu criei um objeto JavaScript via prototipagem. Estou tentando renderizar uma tabela dinamicamente. Embora a parte de renderização seja simples e funcione bem, também preciso lidar com determinados eventos do lado do cliente para a tabela renderizada dinamicamente. Isso também é fácil. Onde estou tendo problemas é com a referência "essa" dentro da função que lida com o evento. Em vez de "isso" faz referência ao objeto, está referenciando o elemento que aumentou o evento.
Veja o código. A área problemática está em 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);
}
Solução
Você precisa "vincular" o manipulador à sua instância.
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);
}
Observe que o manipulador de eventos aqui normaliza event
objeto (passado como um primeiro argumento) e invoca handleCellClick
em um contexto adequado (ou seja, referindo -se a um elemento que foi o ouvinte de eventos anexado).
Observe também que a normalização do contexto aqui (ou seja, configuração adequada this
no manipulador de eventos) cria uma referência circular entre a função usada como manipulador de eventos (onClickBound
) e um objeto de elemento (cell1
). Em algumas versões do IE (6 e 7), isso pode, e provavelmente resultará em um vazamento de memória. Esse vazamento em essência é o navegador que não libera memória na atualização da página devido à referência circular existente entre o objeto nativo e host.
Para contorná -lo, você precisaria a) soltar this
normalização; b) empregar estratégia de normalização alternativa (e mais complexa); c) "limpar" os ouvintes de eventos existentes na página descarregar, ou seja, usando removeEventListener
, detachEvent
e elementos null
ing (que infelizmente tornaria a navegação de história rápida dos navegadores inútil).
Você também pode encontrar uma biblioteca JS que cuida disso. A maioria deles (por exemplo: jQuery, prototype.js, yui, etc.) geralmente lida com as limpezas, conforme descrito em (c).
Outras dicas
Você pode usar ligar o que permite especificar o valor que deve ser usado como isto Para todas as chamadas para uma determinada função.
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
}
Um problema no exemplo acima é que você não pode remover o ouvinte com Bind. Outra solução é usar uma função especial chamada handlevent Para pegar qualquer evento:
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);
}
Como sempre mdn é o melhor :). Acabei de copiar coloquei a peça do que responder a esta pergunta.
Além disso, mais uma maneira é usar o Interface EventListener (De Dom2 !! Querendo -se saber por que ninguém mencionou, considerando que é a maneira mais legal e destinada a essa situação.)
Ou seja, em vez de passar uma função de retorno de chamada, você passa um objeto que implementa a interface EventListener. Simplificando, isso significa apenas que você deve ter uma propriedade no objeto chamado "handlevent", que aponta para a função Handler de eventos. A principal diferença aqui é, dentro da função, this
se referirá ao objeto passado para o addEventListener
. Aquilo é, this.theTicketTable
será a instância do objeto no código abaixo. Para entender o que quero dizer, veja o código modificado com cuidado:
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);
}
Eu sei que este é um post mais antigo, mas você também pode simplesmente atribuir o contexto a uma variável self
, jogue sua função em uma função anônima que invoca sua função com .call(self)
e passa no contexto.
ticketTable.prototype.render = function(element) {
...
var self = this;
cell1.addEventListener('click', function(evt) { self.handleCellClick.call(self, evt) }, false);
...
};
Isso funciona melhor do que a "resposta aceita", porque o contexto não precisa ser atribuído uma variável para toda a classe ou global, mas está bem escondido dentro do mesmo método que ouve o evento.
Fortemente influenciados pela resposta de Kamathln e Gagarine, pensei em enfrentar isso.
Eu estava pensando que você provavelmente poderia ganhar um pouco mais de liberdade se colocar o HandecellClick em uma lista de retorno de chamada e usar um objeto usando a interface EventListener no evento para acionar os métodos da lista de retorno de chamada com o correto.
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);
}
Esta sintaxe de flecha funciona para mim:
document.addEventListener('click', (event) => {
// do stuff with event
// do stuff with this
});
isto será o pai contexto e não o documento contexto
A respeito
...
cell1.addEventListener("click", this.handleCellClick.bind(this));
...
ticketTable.prototype.handleCellClick = function(e)
{
alert(e.currentTarget.innerHTML);
alert(this.tickets.length);
}
E.CurrentTarget aponta para o alvo que está ligado ao "evento de clique" (para o elemento que aumentou o evento) enquanto
vínculo (isso) preserva o valor externo de this
Dentro da função de evento de clique.
Se você deseja clicar um alvo exato, use E.Target em vez de.
Com o ES6, você pode usar uma função de seta como isso usará o escopo lexical [0], o que permite evitar ter que usar 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());
}