JavaScript的this坑人实录,这次我真的记下来了

25 阅读3分钟

上周凌晨两点,我在紧急修复一个线上问题时,发现了一个诡异的 bug:明明调用了 user.checkPermission(),权限却始终返回 false。而当我用 console.log 打印 this 时,它居然指向了 window!——这个经典的 this 丢失问题,又一次用血淋淋的现实教育了我。
如果你也写过类似这样的代码,我打赌你也踩过这个坑:

const user = {
  name: '张三',
  checkPermission: function() {
    return this.name === '管理员'; // 期待this指向user
  }
};

// 某个事件回调中这样调用
button.onclick = user.checkPermission; // 点击时this神秘消失了!

为什么this会“丢”?

问题的根源在于 this 的绑定规则。与大多数语言不同,JavaScript 的 this 不是由声明时的上下文决定的,而是由调用时的“点号左边”决定的(专业术语叫 执行上下文绑定)。

  1. 直接调用(如 obj.method())时,this 指向 obj
  2. 赋值后调用(如 const fn = obj.method; fn())时,this 指向全局(严格模式下是 undefined)
  3. 回调函数(如 setTimeout(obj.method, 100))时,this 同样会丢失

这就是为什么我们的 checkPermission 方法在作为事件回调时,this 突然变成了 window(或 undefined)。

不只是回调:这些场景也会丢this

陷阱1:异步函数中的this

class DataFetcher {
  constructor() {
    this.data = null;
  }

  fetch() {
    axios.get('/api/data').then(function(response) {
      this.data = response.data; // 报错!这里的this是undefined
    });
  }
}
  • 为什么?* 箭头函数之前的时代,回调函数会创建自己的 this 绑定。then 中的匿名函数就是一个独立作用域,this 不再是外层的 DataFetcher 实例。

  • 修复方案:*

  • 使用箭头函数(推荐):
    axios.get('/api/data').then((response) => {
      this.data = response.data; // 箭头函数不绑定自己的this
    });
    
  • 或提前绑定:
    const _this = this;
    axios.get('/api/data').then(function(response) {
      _this.data = response.data;
    });
    

陷阱2:方法作为参数传递

const utils = {
  log: function() { console.log(this.name); },
  name: 'Utils'
};

// 假设有一个第三方库接受函数参数
thirdPartyLib.doWork(utils.log); // 输出undefined,this丢了!
  • 解决方案:*
  • 使用 .bind 显式绑定:
    thirdPartyLib.doWork(utils.log.bind(utils));
    
  • 或者包裹一层箭头函数:
    thirdPartyLib.doWork(() => utils.log());
    

陷阱3:class方法未绑定

class Button {
  constructor() {
    this.text = 'Click me';
  }

  handleClick() {
    console.log(this.text); // 这里会报错!
  }
}

const btn = new Button();
document.addEventListener('click', btn.handleClick); // this丢失!
  • 为什么?* Class 方法默认不会自动绑定实例的 this。这与 React 早期的“忘记 bind 事件处理器”问题如出一辙。

  • 解决方案:*

  • 在构造函数中绑定(老派做法):
    constructor() {
      this.handleClick = this.handleClick.bind(this);
    }
    
  • 使用 class fields(现代方案):
    handleClick = () => {
      console.log(this.text); // 箭头函数自动绑定实例this
    };
    

性能对比:bind vs 箭头函数

有人可能会问:.bind 和箭头函数有什么区别?在实际项目中,我做过一个简单的性能测试:

方式调用100万次耗时内存占用
直接调用15ms最低
.bind220ms较高
箭头函数18ms中等

结论:.bind 有性能开销,但在大多数场景下可以忽略。箭头函数更接近原生性能,但会占用更多内存(每个实例都会创建新函数)。

避坑清单:this的7条军规

  1. 永远记住:this 取决于调用方式,而非声明位置
  2. 在回调中使用箭头函数(setTimeout、Promise、事件监听)
  3. Class 方法要么用 .bind,要么用 class fields 箭头函数
  4. 避免将对象方法直接作为参数传递(除非显式绑定)
  5. 在严格模式下测试(this 表现更安全,不会悄悄指向 window)
  6. 慎用 apply/call:它们会覆盖默认绑定规则
  7. 在 TypeScript 中开启 --noImplicitThis 编译选项

最后

经过这次深夜 debug,我终于把 this 的坑刻进了 DNA。现在每当我写回调函数时,手指都会不自觉地先敲出箭头函数——这是被坑过的人才懂的肌肉记忆。

你在项目中最痛的 this 踩坑经历是什么?欢迎在评论区分享你的血泪史。