'this' 在回调中绑定为闭包,如果以特定顺序调用,则通过 apply 变为未定义

'this' in callback bound as closure via apply becoming undefined if called in specific order

我有一个 JS 函数,我尝试使用 this 应该是点击按钮触发该函数:

function myAction( greeting ) { console.log( greeting + ", the button is :" + this ); this.remove(); }

然后我的页面上有几个按钮,比方说两个(即使页面上只有两个按钮也会发生这种情况;我已经测试过了):

<button id="button-one" class="click-me">Click Me</button>
<button id="button-two" class="click-me">Click Me</button>

根据我的经验,当我想使用需要使用相关元素作为 this 加上其他函数参数的事件侦听器回调时;如果我们有几个按钮共享例如相同的 class;达到此目的的最佳方法是将该回调绑定为闭包(将迭代元素捕获为 this + apply(传递相关的 this 元素):

var myButtons = document.getElementsByClassName( "click-me" );
var amountOfButtons = myButtons.length;
for ( var i = 0; i < amountOfButtons; i++ ) {
    myButtons[i].addEventListener( "click", (
      function(i) {
        return function() { myAction.apply( myButtons[i], ["hello"] ); };
      }(i))
    );
  }

我对这种方法从来没有任何问题;它总是有效。这实际上是错误的方法吗?

因为,我现在面临的是,当我点击#button-one,然后点击#button-two时,this实际上在严格模式下变成了undefined并且window 在非严格模式下;在第二次点击。当我反过来做时,this 总是坚持成为相应点击的按钮。它似乎以某种方式更新了 myButtons 集合,这样当第一个按钮通过第一次调用被删除时, 1 索引在集合中变得不可用,因为只剩下一个按钮;另一个被删除。至少那是我的理论。但是我以前从未在类似的代码案例中遇到过这个问题?那我该如何编码我想要发生的事情呢?

编辑

感谢大家;并特别感谢@Nenad Vracar,因为他的解决方案导致所需的最少编辑,只需更改即可完美运行:

var myButtons = document.getElementsByClassName( "click-me" );
var amountOfButtons = myButtons.length;
for ( var i = 0; i < amountOfButtons; i++ ) {
    myButtons[i].addEventListener( "click", (
      function(i) {
        return function() { myAction.apply( myButtons[i], ["hello"] ); };
      }(i))
    );
  }

收件人:

var myButtons = document.getElementsByClassName( "click-me" );
var amountOfButtons = myButtons.length;
for ( var i = 0; i < amountOfButtons; i++ ) {
    myButtons[i].addEventListener( "click", function() {
      myAction.apply( this, ["hello"] );
    });
  }

简单的智能解决方案,我简直是见树不见林!再次感谢!

注意。事件委托在这里并不是一个真正的选择,因为页面的 DOM 结构/需要附件的元素有些复杂,我通常更喜欢将函数附加到需要它的元素。另外,我宁愿不将函数代码更改为闭包本身,也不要更改函数代码本身。因此,我决定使用此解决方案的原因。

您可以在使用 querySelectorAll 的地方使用稍微不同的方法,然后您可以在其上使用 forEach 循环。在点击事件处理程序中,您不需要额外的闭包,myAction 中的 this 将是点击事件处理程序中的 this,因为您将其作为 thisArg 传递apply.

的参数

function myAction(greeting) {
  console.log(greeting + ", the button is :" + this.textContent);
  this.remove();
}

document
  .querySelectorAll(".click-me")
  .forEach(function(btn) {
    btn.addEventListener('click', function() {
      myAction.apply(this, ['Hello'])
    })
  })
<button id="button-one" class="click-me">One</button>
<button id="button-two" class="click-me">Two</button>

您现在的方法似乎很复杂。这里有两个简化流程的建议。

高阶函数事件处理程序

如果你re-definemyAction作为一个函数即returns事件监听器,你可以直接传递参数。因此,分配处理程序时无需将其包装在另一个函数中:

function myAction( greeting ) { //take input
  return function() { //return the function to act as event listener
    console.log( greeting + ", the button is :" + this ); this.remove(); 
  }
}

var myButtons = document.getElementsByClassName( "click-me" );
var amountOfButtons = myButtons.length;

for ( var i = 0; i < amountOfButtons; i++ ) {
    //pass a variable to be used in the listener
    myButtons[i].addEventListener( "click", myAction( "hello" ) );
}
<button id="button-one" class="click-me">Click Me</button>
<button id="button-two" class="click-me">Click Me</button>

使用事件委托

这是首选方法。您可以将单个事件侦听器添加到 DOM 树中的共同祖先,而不是向 每个 元素添加事件侦听器。那么你只需要一个功能。

值得注意的是,事件监听器会接收所有children的事件,因此需要过滤掉不相关的。你可以制作一个简单的包装器来检查目标目标是否正确,然后执行装饰处理程序:

function myAction( greeting ) { 
  return function(event) { //take an event 
    var el = event.target; //extract the element from it
    console.log( greeting + ", the button is :" + el ); el.remove(); //use the element
  }
}

/*
 * Convert a handler to a delegate to only work on a sub-section of elements
 * @param {string} filterSelector - the handler will only be triggered
 *   for elements matching this selector
 * @param {Function} handler - the handler to wrap
 *
 * @return {Function} a function to serve as a delegated event handler
 */
function delegate( filterSelector, handler ) {
  return function ( event ) {
    //only fire for explicitly desired targets
    if (event.target.matches( filterSelector ) ) {
      //forward call to the handler
      handler.apply( this, arguments );
    }
  }
}


var delegatedHandler = delegate( ".click-me", myAction( "hello" ) );

document.body //attach higher up in the DOM
  .addEventListener( "click", delegatedHandler );
<button id="button-one" class="click-me">Click Me</button>
<button id="button-one" class="dummy">I should stay</button>
<button id="button-two" class="click-me">Click Me</button>

如果您使用的是旧版浏览器,请参阅 Element#matches() 了解潜在的 polyfill。

另请参阅:
What is DOM Event delegation?
Event binding on dynamically created elements?