“ this”在使用 addEventListener 的处理程序中的值

我已经通过原型创建了一个 Javascript 对象。我试图动态地呈现一个表。虽然呈现部分很简单并且工作正常,但是我还需要为动态呈现的表处理某些客户端事件。那也很容易。我遇到的问题是处理事件的函数内部的“ this”引用。不是“ this”引用对象,而是引用引发事件的元素。

查看代码。问题区域在 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);
}
131094 次浏览

You need to "bind" handler to your 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);
}

Note that event handler here normalizes event object (passed as a first argument) and invokes handleCellClick in a proper context (i.e. referring to an element that was attached event listener to).

Also note that context normalization here (i.e. setting proper this in event handler) creates a circular reference between function used as event handler (onClickBound) and an element object (cell1). In some versions of IE (6 and 7) this can, and probably will, result in a memory leak. This leak in essence is browser failing to release memory on page refresh due to circular reference existing between native and host object.

To circumvent it, you would need to either a) drop this normalization; b) employ alternative (and more complex) normalization strategy; c) "clean up" existing event listeners on page unload, i.e. by using removeEventListener, detachEvent and elements nulling (which unfortunately would render browsers' fast history navigation useless).

You could also find a JS library that takes care of this. Most of them (e.g.: jQuery, Prototype.js, YUI, etc.) usually handle cleanups as described in (c).

I know this is an older post, but you can also simply assign the context to a variable self, throw your function in an anonymous function that invokes your function with .call(self) and passes in the context.

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

This works better than the "accepted answer" because the context doesn't need to be assigned a variable for the entire class or global, rather it's neatly tucked away within the same method that listens for the event.

Also, one more way is to use the EventListener Interface (from DOM2 !! Wondering why no one mentioned it, considering it is the neatest way and meant for just such a situation.)

I.e, instead of a passing a callback function, You pass an object which implements EventListener Interface. Simply put, it just means you should have a property in the object called "handleEvent" , which points to the event handler function. The main difference here is, inside the function, this will refer to the object passed to the addEventListener. That is, this.theTicketTable will be the object instance in the belowCode. To understand what I mean, look at the modified code carefully:

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

You can use bind which lets you specify the value that should be used as this for all calls to a given function.

   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
}

A problem in the example above is that you cannot remove the listener with bind. Another solution is using a special function called handleEvent to catch any events:

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

Like always mdn is the best :). I just copy pasted the part than answer this question.

Heavily influenced by kamathln and gagarine's answer I thought I might tackle this.

I was thinking you could probably gain a bit more freedom if you put handeCellClick in a callback list and use an object using the EventListener interface on the event to trigger the callback list methods with the correct this.

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

What about

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


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

e.currentTarget points to the target which is bound to the "click event" (to the element that raised the event) while

bind(this) preserves the outerscope value of this inside the click event function.

If you want to get an exact target clicked, use e.target instead.

With ES6, you can use an arrow function as that will use lexical scoping[0] which allows you to avoid having to use bind or 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

This arrow syntax works for me:

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

this will be the parent context and not the document context.

According to https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener ,

my_element.addEventListener('click', (e) => {
console.log(this.className)           // WARNING: `this` is not `my_element`
console.log(e.currentTarget === this) // logs `false`
})

so if you use the arrow functions you can go safe beacause they do not have their own this context.

The MDN explanation gives what to me is a neater solution further down.

In this example you store the result of the bind() call, which you can then use to unregister the handler later.

const Something = function(element) {
// |this| is a newly created object
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 bound to newly created object
};


// bind causes a fixed `this` context to be assigned to onclick2
this.onclick2 = this.onclick2.bind(this);


element.addEventListener('click', this.onclick1, false);
element.addEventListener('click', this.onclick2, false); // Trick
}
const s = new Something(document.body);

In the posters example you would want to bind the handler function in the constructor:

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


this.handleCellClick = this.handleCellClick.bind(this); // Note, this means that our handleCellClick is specific to our instance, we aren't directly referencing the prototype any more.
}


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
this.handleCellClick = this.handleCellClick.bind(this); // Note, this means that our handleCellClick is specific to our instance, we aren't directly referencing the prototype any more.
cell1.addEventListener("click", this.handleCellClick, false);


// We could now unregister ourselves at some point in the future with:
cell1.removeEventListener("click", this.handleCellClick);


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


}