addEventListener中处理程序中的"this"值

109
我通过原型创建了一个Javascript对象。我正在尝试动态渲染一个表格。虽然渲染部分很简单并且可以正常工作,但我还需要处理动态渲染表格的某些客户端事件。这也很容易。我的问题在于处理事件的函数内部的"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);
    }
11个回答

114
你可以使用 bind ,它可以让你指定在调用给定函数时应该被用作this的值。
   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
    }

上面示例中的问题是您不能使用bind函数来移除监听器。另一种解决方法是使用一个名为handleEvent的特殊函数来捕获任何事件:

上述问题是无法使用bind函数来移除监听器,可使用名为handleEvent的特殊函数捕获所有事件来解决。

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

一如既往,MDN 是最好的 :)。我只是将部分内容复制粘贴,然后回答这个问题。


51
您需要将处理程序“绑定”到实例上。
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);
}
请注意,这里的事件处理程序对传递的事件对象(作为第一个参数)进行了规范化,并在正确的上下文中调用了handleCellClick(即指向已附加事件侦听器的元素)。
还要注意,在此处进行的上下文规范化(即在事件处理程序中设置适当的this)会在事件处理程序(onClickBound)和元素对象(cell1)之间创建循环引用。在某些版本的IE(6和7)中,这可能会导致内存泄漏。实质上,这种泄漏是因为原生和宿主对象之间存在循环引用而导致浏览器在页面刷新时无法释放内存。
为了避免这种情况,您需要a)放弃this规范化;b)采用替代(更复杂)的规范化策略;c)在页面卸载时“清除”现有的事件侦听器,即使用removeEventListenerdetachEvent和元素null(这不幸地会使浏览器快速历史记录导航无效)。
您还可以找到一个JS库来解决这个问题。大多数JS库(例如:jQuery、Prototype.js、YUI等)通常会处理如(c)所述的清理工作。

在我的代码上下文中,var _this = this;应该放在哪里?我需要将onClickBound(e)添加到原型中吗? - Darthg8r
render 函数中,在附加事件监听器之前,您可以使用此代码段将原始示例中的 addEventListener 行替换掉。 - kangax
有趣的是你提到了清理。实际上,在这个过程中,我也会摧毁这些对象。最初,我计划只是做.innerHTML = ""; 我猜在这种情况下这样做是不好的。如何销毁这些表格并避免泄漏呢? - Darthg8r
正如我之前所说,查看 removeEventListener/detachEvent 并解决循环引用。这里有一个关于泄漏的很好的解释 - http://www.jibbering.com/faq/faq_notes/closures.html#clMem - kangax
5
我不知道为什么,但这个“自身=这些技巧”总让我感觉不对。 - gagarine

15

这个箭头语法对我有用:

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

this将是父级上下文,而不是文档上下文。


2
你如何使用removeEventListener来移除事件监听器? - pixelearth
@pixelearth,你能不能不把箭头函数引用到一个变量中,并在add和remove中使用该变量进行引用? - Koray Tugay
1
@KorayTugay是可以的,但我指出了这个答案结构的问题。 - pixelearth

14

还有一种方法是使用EventListener接口(来自DOM2 !!想知道为什么没有人提到它,考虑到它是最整洁的方式并且正好适合这种情况。)

也就是说,不是传递回调函数,而是传递实现了EventListener接口的对象。简单地说,它只意味着你应该在对象中有一个名为“handleEvent”的属性,该属性指向事件处理程序函数。主要区别在于,在函数内部,this将引用传递给addEventListener的对象。也就是说,this.theTicketTable将成为下面代码中的对象实例。要理解我的意思,请仔细查看修改后的代码:

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

挺不错的,但是好像不能用这个方法removeEventListener() - knutole
3
@knutole,是的,你可以。只需要将对象保存在变量中,然后将该变量传递给addEventListener函数即可。你可以参考这里:https://dev59.com/QnDYa4cB1Zd3GeqPBYly#15819593。 - tomekwi
TypeScript似乎不喜欢这种语法,因此它不会编译带有对象作为回调的addEventListener调用。该死。 - David R Tribble
@DavidRTribble 你试过将对象分配给变量,然后传递变量吗?我个人没有尝试过TypeScript,但如果它确实是一个语法问题而不是“函数不接受参数”类型的问题,那么这可能是一个解决方案。 - kamathln

9

在 ES6 中,你可以使用箭头函数作为函数的绑定环境是词法作用域[0],这样就可以避免使用 bindself = 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] 学习ES6的“毒瘤”方式第二部分:箭头函数和this关键字


2
你如何使用removeEventListener来移除事件监听器? - pixelearth
@pixelearth 你不需要这样做。 - Zach Saucier
我的意思正是如此,只不过我没有说得那么准确。 - pixelearth

8

4
e.currentTarget 确实是我们许多人所需要的 :) - Yan King Yin

5
我知道这是一个较旧的帖子,但你也可以将上下文分配给变量self,将函数放入匿名函数中,并使用.call(self)调用您的函数并传递上下文。
ticketTable.prototype.render = function(element) {
...
    var self = this;
    cell1.addEventListener('click', function(evt) { self.handleCellClick.call(self, evt) }, false);
...
};

这比“被接受的答案”更好,因为上下文不需要被赋给整个类或全局变量,而是被妥善地隐藏在监听事件的同一方法中。

2
使用ES5,您只需使用以下代码即可获得相同的效果:cell1.addEventListener('click', this.handleCellClick.bind(this));。如果您想要与FF <=5兼容,请将最后一个参数false保留。 - tomekwi
问题在于,如果您从处理程序调用其他函数,则需要将self传递到下一级。 - mkey
这是我使用的解决方案。我本来更喜欢使用.bind()方法,但不得不使用.call(),因为我们的应用程序还需要支持IE8一段时间。 - David R Tribble
2
以前我使用的是 self,直到我发现 selfwindow 是一样的,现在我使用 me - user1663023
2
你如何使用 removeEventListener 这个方法? - Tamb

2

关于什么?

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

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

e.currentTarget 指向绑定了 "click 事件" 的目标元素(触发事件的元素),而 bind(this) 会在 click 事件处理函数内部保留外部作用域中 this 的值。

如果想要获取精确点击的目标元素,请使用 e.target


1
受 kamathln 和 gagarine 的回答的影响,我想我可以尝试解决这个问题。 我认为,如果您将 handeCellClick 放入回调列表中,并使用实现 EventListener 接口的对象在事件上触发回调列表方法并正确使用 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);
    }

0

MDN explanation提供了一个更简洁的解决方案。

在这个例子中,你可以存储bind()调用的结果,然后稍后使用它来注销处理程序。

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

在海报的例子中,您需要在构造函数中绑定处理程序函数:
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);

    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接