深入解析箭头函数:彻底搞懂定时器与事件监听中的 this 问题!

275 阅读6分钟

箭头函数从定义它的最近的外层函数作用域中继承 this

箭头函数没有自己的 this 上下文,箭头函数从定义它的最近的外层函数作用域中继承 this

借助这一句话可以理解所有涉及到箭头函数的this值,箭头函数的this值永远不会改变。

定时器

定时器中的this值

相信大家都听说过一句比较笼统的概括:定时器中的this值指向window。反正写这篇文章之前我一直都以为是这样的。为什么说它笼统呢?当然,它并不全面,甚至可能造成误解。

先上结论:

  • 定时器的普通函数的 this 值指向 window\undefined
  • 定时器的箭头函数的 this 保留为定义它时所在的词汇作用域。

看一段代码:

const obj = {
  name: 'obj',
  fn() {
    setTimeout(function() {
      console.log(this);// window
    }, 0)
    console.log(this === obj);// true
    let that = this;
    setTimeout(() => {
      console.log(this === that);// true
      console.log(this === obj);// true
      console.log(this.name);// obj
    }, 0);
  }
}
obj.fn();

分析第一个定时器:

第一个定时器调用普通函数。至于普通函数不是我们今天的重点,下边这段分析也讲的够清楚:

所有超时执行的函数都会在全局作用域中的一个匿名函数中运行,因此函数中的 this 值在非严格模式下始终指向 window

分析第二个定时器:

  1. 第二个定时器调用箭头函数,按照上边的结论,箭头函数从定义它的最近的外层函数作用域中继承this,就是说箭头函数的this值继承了fn函数的this值
  2. fn函数的this值是什么呢?fn函数这里是普通函数,且被obj对象所拥有,通过obj.fn()调用,属于隐式绑定,其this值就是obj对象。
  3. 我们将fn函数的this值保存在that变量中(为什么要这样做呢?任何内部函数都不能直接访问到外部函数的this对象)与箭头函数的this值比较,和预想的一样。箭头函数内部的this值等于外部fn函数的this值,也等于obj对象。

定时器中使用箭头函数

刚刚一通分析,看来定时器中传一个普通函数好像没啥用,this值一直指向window也不是个事。如果我想要用到上下文作用域中的变量怎么搞?使用箭头函数✌!

使用箭头函数的最大好处可能是在使用 setTimeout()和 EventTarget.prototype.addEventListener()等方法时,这些方法通常需要某种闭包、call()apply() 或 bind(),以确保函数在适当的作用域中执行。

示例1

对于传统的函数表达式,类似这样的代码并不能像预期的那样工作:

const obj = {
  count: 10,
  doSomethingLater() {
    setTimeout(function () {
      // 此函数在 window 作用域下执行
      this.count++;
      console.log(this.count);
    }, 300);
  },
};

obj.doSomethingLater(); // 输出“NaN”,因为“count”属性不在 window 作用域下。

有了箭头函数,this 作用域更容易被保留:

const obj = {
  count: 10,
  doSomethingLater() {
    // 该方法语法将“this”与“obj”上下文绑定。
    setTimeout(() => {
      // 由于箭头函数没有自己的绑定,
      // 而 setTimeout(作为函数调用)本身也不创建绑定,
      // 因此使用了外部方法的“obj”上下文。
      this.count++;
      console.log(this.count);
    }, 300);
  },
};

obj.doSomethingLater(); // 输出 11

示例2:防抖

function handleNameSearch(e) {
  console.log(this);
  ...
}

function debounce(func, delay) {
  let timeoutId;
  return function (...args) {
    console.log(this);// inputb元素的引用
    console.log(this === inputb);// true
        
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => {
      // func.apply(this, args);
      func(...args);
    }, delay);
  }
}

const debounceNameSearch = debounce(handleNameSearch, 500);
inputa.addEventListener('input', handleNameSearch);
inputb.addEventListener('input', debounceNameSearch);
  1. 先注释掉第10行的apply(), 使用第11行调用handleNameSearch函数。我们这里有两个输入框,第一个没有防抖效果,第二个防抖处理。分别触发一次输出handleNameSearch函数内的this值:分别输出了input元素的引用和window。很明显,处理防抖时把this搞丢了
  2. 事件监听函数内部的this值是触发事件的元素的引用(第二部分讲),所以防抖函数返回的函数内部的this值就是元素的引用(代码9-10行已经输出验证了)
  3. 这样就好办了,我们现在要以这个this值来调用handleNameSearch: func.apply(this, args)
  4. 箭头函数的this值继承的刚好是我们想要的this。
  5. 这里要是使用普通函数也倒可以实现,可以将外层函数的this保存在一个变量里,这就相对比较麻烦了。

事件监听函数

事件监听函数中的this值

当使用 addEventListener() 为一个元素注册事件的时候,事件处理器里的 this值是该元素的引用。其与传递给句柄的 event 参数的 currentTarget 属性的值一样。当然,这只针对传统函数。

my_element.addEventListener("click", function (e) {
  console.log(this.id); // 输出 my_element 的 id
  console.log(e.currentTarget === this); // 输出 `true`
});
my_element.addEventListener("click", (e) => {
  console.log(this.id); // 警告:`this` 并不指向 `my_element`
  console.log(e.currentTarget === this); // 输出 `false`
});

总结

  1. 箭头函数的 this 继承机制

    • 箭头函数没有自己的 this 上下文,它从定义它的最近的外层函数作用域中继承 this
    • 箭头函数的 this 值不会改变,即使在不同的上下文中调用,this 仍然是定义它时所在作用域的 this
  2. 定时器中的 this

    • 在定时器中使用普通函数时,this 默认指向全局对象 window(或在严格模式下为 undefined)。
    • 在定时器中使用箭头函数时,this 保留为定义箭头函数时的作用域的 this,不会指向 window,而是继承外层函数的 this
  3. 使用箭头函数解决 this 问题

    • 定时器和事件处理器中的 this 往往会变成 windowundefined。使用箭头函数可以避免 this 丢失的问题,它能够从定义时的上下文继承正确的 this
    • 在需要保持上下文 this 的情况下,箭头函数是一个很好的选择,避免了手动 bind() 或通过变量保存 this 的麻烦。
  4. 事件监听器中的 this

    • 对于普通函数,事件处理器中的 this 指向触发事件的 DOM 元素,与 event.currentTarget 一致。
    • 使用箭头函数时,this 不再指向触发事件的元素,而是继承自定义时的外层作用域的 this,因此不会指向 event.currentTarget
  5. 常见的用法和场景

    • 使用箭头函数处理防抖和节流时,它继承外层的 this,可以更方便地操作元素或上下文对象。
    • 在事件处理和定时器回调中,使用箭头函数能够确保 this 指向正确的对象,避免意外指向全局对象 window