什么是 this?

简单来说,this 是一个在函数运行时自动生成的特殊变量,它指向函数的“当前所有者”。这个“所有者”会根据函数的调用方式不同而变化。这就是 this 让人困惑的根本原因:它不是固定不变的,而是动态决定的。

为了理解 this,我们可以从第一性原理出发:JavaScript 设计 this 的初衷是为了让函数能够根据上下文动态访问调用它的对象。 接下来,我们通过四种主要绑定规则来逐一拆解 this 的指向。

this 的四种绑定规则

1. 默认绑定:全局对象或 undefined

当一个函数被独立调用(不依赖任何对象)时,this 默认指向全局对象。在浏览器中,全局对象是 window;在 Node.js 中则是 global。来看个例子:

function showThis() {
  console.log(this)
}

showThis() // 浏览器中输出 window,Node.js 中输出 global

但如果开启了严格模式('use strict'),情况就变了:

'use strict'
function showThis() {
  console.log(this)
}

showThis() // 输出 undefined

为什么会这样? 严格模式是为了避免意外修改全局对象,所以 this 在独立调用时被设置为 undefined。这个规则提醒我们:独立调用的函数,this** 默认指向全局对象,但在严格模式下要小心它是 undefined。**

2. 隐式绑定:谁调用我,我指向谁

当函数作为对象的方法被调用时,this 指向调用该方法的对象。这是最常见的情况:

const user = {
  name: '小明',
  greet() {
    console.log(`你好,我是 ${this.name}`)
  }
}

user.greet() // 输出:你好,我是小明

这里的 greetuser 对象的方法,调用时 this 指向 user,所以能访问到 name 属性。

但要小心一个常见陷阱:如果把方法赋值给一个变量再调用,this 会丢失:

const sayHi = user.greet
sayHi() // 输出:你好,我是 undefined

为什么会这样? 因为 sayHi() 是独立调用,不再依附于 user 对象,this 又回到了默认绑定的规则(全局对象或 undefined)。解决办法 是后面会提到的 bind 或箭头函数。

3. 显式绑定:我来指定 this

JavaScript 提供了 callapplybind 三种方法,让你明确指定 this 的值。这种方式非常灵活,适合需要动态改变 this 指向的场景:

function introduce(hobby, food) {
  console.log(`我是 ${this.name},我喜欢 ${hobby} 和吃 ${food}`)
}

const person = { name: '小红' }

introduce.call(person, '跑步', '火锅') // 输出:我是小红,我喜欢跑步和吃火锅
introduce.apply(person, ['游泳', '寿司']) // 输出:我是小红,我喜欢游泳和吃寿司

const introduceLiLei = introduce.bind({ name: '李雷' })
introduceLiLei('编程', '披萨') // 输出:我是李雷,我喜欢编程和吃披萨
  • callapply 都会立即调用函数,区别在于参数传递方式:call 是一个个传,apply 是以数组形式传。
  • bind 不会立即调用,而是返回一个新函数,固定了 this 的值。

适用场景:当你需要临时改变 this 指向,或者想复用一个函数但让它在不同对象上工作时,显式绑定是你的好帮手。

4. new 绑定:构造函数中的 this

当用 new 关键字调用构造函数时,this 指向新创建的实例对象:

function Person(name) {
  this.name = name
  this.sayName = function() {
    console.log(`我是 ${this.name}`)
  }
}

const xiaoming = new Person('小明')
xiaoming.sayName() // 输出:我是小明

为什么会这样? 使用 new 时,JavaScript 会创建一个新对象,并将构造函数中的 this 绑定到这个新对象上。这让构造函数可以初始化对象的属性。

箭头函数:this 的特殊情况

箭头函数(=>)是 ES6 引入的,它对 this 的处理方式完全不同:箭头函数没有自己的 this,它会继承外层作用域的 this 值。

来看一个对比:

const obj = {
  name: '小方',
  sayLater1: function() {
    setTimeout(function() {
      console.log(`普通函数: 你好,${this.name}`)
    }, 1000)
  },
  sayLater2: function() {
    setTimeout(() => {
      console.log(`箭头函数: 你好,${this.name}`)
    }, 1000)
  }
}

obj.sayLater1() // 输出:普通函数: 你好,undefined
obj.sayLater2() // 输出:箭头函数: 你好,小方
  • sayLater1 中,setTimeout 里的普通函数是独立调用的,this 指向全局对象(或 undefined)。
  • sayLater2 中,箭头函数继承了外层 sayLater2this,即 obj,所以能正确访问 name

关键点:箭头函数的 this 在定义时就固定了,不会因为调用方式改变。这让它特别适合用在回调函数中,避免 this 丢失的问题。

常见陷阱和解决方案

1. 回调函数中的 this 丢失

回调函数是 this 容易出问题的地方,比如:

const counter = {
  count: 0,
  increase: function() {
    console.log(this.count++)
  }
}

setTimeout(counter.increase, 1000) // 输出:NaN

这里 counter.increase 被作为回调函数传递,调用时失去了 counter 的上下文,this 变成了全局对象,导致 countundefined

解决方案 有两种:

  • 使用箭头函数:箭头函数继承外层 this,可以直接访问 counter
setTimeout(() => counter.increase(), 1000) // 输出:0
  • 使用 bind:在传递函数时绑定 this
setTimeout(counter.increase.bind(counter), 1000) // 输出:0

2. DOM 事件处理器中的 this

在 DOM 事件处理中,普通函数的 this 指向触发事件的元素:

const button = document.querySelector('button')
button.addEventListener('click', function() {
  console.log(this) // 输出:<button> 元素
})

但如果用箭头函数,this 会指向外层作用域:

button.addEventListener('click', () => {
  console.log(this) // 输出:window 或 undefined
})

建议:如果需要访问触发事件的元素,用普通函数;如果需要外层作用域的 this,用箭头函数。

如何判断 this 的指向?

面对复杂的代码,如何快速判断 this 指向哪里?可以用这个简单的方法:

  1. 找到函数的调用方式:看函数是怎么被调用的(直接调用、对象方法、new、call/apply/bind)。
  2. 按优先级判断
    • new 绑定优先级最高,this 指向新对象。
    • 其次是显式绑定(callapplybind)。
    • 然后是隐式绑定(对象方法调用)。
    • 最后是默认绑定(全局对象或 undefined)。
  3. 特殊情况:如果是箭头函数,找它定义时外层的 this

总结

this 的核心在于它是一个动态绑定的变量,取决于函数的调用方式。掌握了四种绑定规则(默认绑定、隐式绑定、显式绑定、new 绑定)和箭头函数的特性,你就能在大多数场景下游刃有余。遇到问题时,记得用 bind 或箭头函数来解决 this 丢失的麻烦。