JavaScript this 指向详解:五种绑定规则、优先级与常见面试陷阱
在 JavaScript 中,this 是一个非常重要、也是前端面试经常考察的知识点。
很多人容易认为,this 指向函数所在的对象,但实际上并不完全正确。
普通函数的 this****主要由函数的调用方式决定,而不是函数定义的位置;箭头函数则使用定义时外层作用域的 this**。**
理解这一区别,就能掌握绝大多数 this 指向问题。
什么是 this?
this 是 JavaScript 提供的一个特殊关键字,用于在函数执行过程中访问当前的调用上下文。
例如:
javascript
const user = {
name: "Tom",
sayHi() {
console.log(this.name)
}
}
user.sayHi() // Tom
这里通过 user.sayHi() 调用函数,因此 this 指向 user,相当于访问 user.name。
但如果改变函数的调用方式,即使是同一个函数,this 的指向也可能发生变化。
this 的五种指向规则
JavaScript 中常见的 this 绑定方式如下:
| 绑定方式 | 调用示例 | this 指向 |
|---|---|---|
| new 绑定 | new Person() |
新创建的实例 |
| 显式绑定 | call/apply/bind |
指定的对象 |
| 隐式绑定 | obj.fn() |
调用方法的对象 |
| 默认绑定 | fn() |
严格模式为 undefined,非严格模式通常为全局对象 |
| 箭头函数 | () => {} |
定义时外层作用域的 this |
需要注意,前四种主要针对普通函数,而箭头函数没有自己的 this,因此不适用普通函数的绑定规则。
new 绑定
当使用 new 调用构造函数时,JavaScript 会创建一个新对象,并让函数执行时的 this 指向该对象。
javascript
function Person(name) {
this.name = name
}
const p = new Person("Tom")
console.log(p.name) // Tom
执行 new Person("Tom") 时,会创建实例对象 p,并通过 this.name = name 给它添加属性。
因此,new 绑定的 this 指向新创建的实例。
显式绑定:call、apply、bind
普通函数可以通过 call()、apply() 和 bind() 显式指定 this。
例如:
javascript
function greet() {
console.log(this.name)
}
const user = {
name: "Tom"
}
greet.call(user) // Tom
这里的 greet.call(user) 表示执行 greet 时,让 this 指向 user。
三个方法的区别主要在于传参方式和执行时机。
| 方法 | 参数形式 | 是否立即执行 |
|---|---|---|
call() |
逐个传入参数 | 是 |
apply() |
使用数组传参 | 是 |
bind() |
返回绑定 this 的新函数 | 否 |
例如:
javascript
function greet(age, city) {
console.log(this.name, age, city)
}
const user = { name: "Tom" }
// call:逐个传参
greet.call(user, 18, "北京")
// apply:数组传参
greet.apply(user, [18, "北京"])
// bind:返回新函数
const fn = greet.bind(user)
fn(18, "北京")
三种方式都可以输出:
Tom 18 北京
其中 bind() 最重要的特点是不会立即执行函数,而是返回一个绑定了 this 的新函数。
隐式绑定:对象方法调用
当普通函数通过对象调用时,this 通常指向调用该方法的对象。
javascript
const user = {
name: "Tom",
greet() {
console.log(this.name)
}
}
user.greet() // Tom
这里 greet() 是通过 user 调用的,所以 this 指向 user。
但函数的定义位置并不能决定 this。
例如:
javascript
const user = {
name: "Tom",
greet() {
console.log(this.name)
}
}
const obj = {
name: "Jack",
greet: user.greet
}
obj.greet() // Jack
虽然 greet 最初定义在 user 中,但现在通过 obj.greet() 调用,所以 this 指向 obj。
这说明:普通函数的 this 取决于调用者,而不是函数最初属于哪个对象。
默认绑定:直接调用函数
如果普通函数没有通过对象、new 或显式绑定方式调用,就会使用默认绑定。
javascript
function sayHi() {
"use strict"
console.log(this)
}
sayHi() // undefined
严格模式下,直接调用普通函数时,this 为 undefined。
非严格模式下,普通函数直接调用时,this 通常指向全局对象,例如浏览器普通脚本环境中的 window。
因此,默认绑定还需要结合严格模式和运行环境判断。
箭头函数:继承外层 this
箭头函数与普通函数最大的区别在于:
箭头函数没有自己的 this,它使用定义时外层作用域的 this。
例如:
javascript
const user = {
name: "Tom",
greet() {
const fn = () => {
console.log(this.name)
}
fn()
}
}
user.greet() // Tom
这里通过 user.greet() 调用普通函数,所以 greet 中的 this 指向 user。
而内部的箭头函数 fn 没有自己的 this,因此使用外层 greet 的 this,最终输出 "Tom"。
但是:
const user = {
name: "Tom",
greet: () => {
console.log(this.name)
}
}
user.greet()
这里的箭头函数不会因为定义在 user 对象中,就让 this 指向 user。
它使用的是对象字面量外层作用域的 this,因此输出结果取决于具体环境。
所以,如果对象方法需要通过 this 访问对象本身,通常应该使用普通函数,而不是箭头函数。
this 的绑定优先级
对于普通函数,通常可以按照以下优先级理解:
new 绑定
↓
显式绑定(call / apply / bind)
↓
隐式绑定(obj.fn())
↓
默认绑定(fn())
例如:
javascript
function greet() {
console.log(this.name)
}
const obj1 = {
name: "Tom",
greet
}
const obj2 = {
name: "Jack"
}
obj1.greet() // Tom
obj1.greet.call(obj2) // Jack
第二次调用虽然使用了 obj1.greet,但通过 call(obj2) 显式指定了 this,因此输出 "Jack"。
另外,bind() 绑定后的普通函数不能通过再次使用 call() 改变已绑定的 this,但通过 new 调用可构造的绑定函数时,new 绑定优先。
javascript
function Person(name) {
this.name = name
}
const obj = {}
const BoundPerson = Person.bind(obj)
const p = new BoundPerson("Tom")
console.log(p.name) // Tom
console.log(obj.name) // undefined
这里 new 创建了新的实例,因此 this 指向新实例,而不是 bind() 中指定的 obj。
常见陷阱:函数作为回调时 this 丢失
例如:
const user = {
name: "Tom",
greet() {
console.log(this.name)
}
}
const fn = user.greet
fn()
这里不能认为 this 仍然指向 user。
因为:
const fn = user.greet
只是把函数赋值给另一个变量,并没有将 user 一起绑定。
随后执行:
fn()
属于直接调用,所以使用默认绑定。
同样的问题经常出现在定时器中:
setTimeout(user.greet, 100)
传给 setTimeout 的是函数本身,不再是通过 user.greet() 调用,因此不能依赖它的 this 指向 user。
可以通过箭头函数解决:
setTimeout(() => {
user.greet()
}, 100)
或者使用 bind():
setTimeout(user.greet.bind(user), 100)
两种方式都能让 greet 正确访问 user.name。
常见陷阱:嵌套函数不会自动继承 this
再看一个例子:
javascript
const obj = {
name: "Tom",
outer() {
console.log(this.name)
function inner() {
"use strict"
console.log(this)
}
inner()
}
}
obj.outer()
输出:
Tom
undefined
因为 outer() 通过 obj 调用,所以 outer 的 this 指向 obj。
但内部的 inner() 是普通函数,并且直接调用,因此严格模式下 this 为 undefined。
如果希望内部函数使用外层的 this,可以改成箭头函数:
javascript
const obj = {
name: "Tom",
outer() {
const inner = () => {
console.log(this.name)
}
inner()
}
}
obj.outer() // Tom
由于箭头函数使用外层 outer 的 this,因此能够正确输出 "Tom"。
面试如何回答 this 的指向规则?
如果面试官问 JavaScript 中 this 的指向规则,可以这样回答:
JavaScript 普通函数的 this 主要由调用方式决定,常见绑定规则包括默认绑定、隐式绑定、显式绑定和 new 绑定。
直接调用普通函数通常使用默认绑定;通过对象调用时,this 指向调用对象;通过 call、apply、bind 可以显式指定 this;使用 new 调用构造函数时,this 指向新创建的实例。
其中,一般情况下绑定优先级为:
new 绑定 > 显式绑定 > 隐式绑定 > 默认绑定。
箭头函数比较特殊,它没有自己的 this,而是使用定义时外层作用域的 this,无法通过 call、apply 或 bind 改变,也不能用作构造函数。
总结
JavaScript 中 this 的核心并不是判断函数在哪里定义,而是判断函数怎样调用。
| 调用方式 | 示例 | this 指向 |
|---|---|---|
| new 绑定 | new Person() |
新实例 |
| 显式绑定 | fn.call(obj) |
指定对象 |
| 隐式绑定 | obj.fn() |
调用对象 |
| 默认绑定 | fn() |
取决于严格模式和运行环境 |
| 箭头函数 | () => {} |
外层作用域的 this |
实际开发中,需要特别注意对象方法作为回调传递时可能丢失 this,以及普通嵌套函数不会自动继承外层函数的 this。
最后记住两句话:
普通函数:this 主要由调用方式决定。
箭头函数:没有自己的 this,使用定义时外层作用域的 this。
掌握这两条核心原则,再结合绑定优先级,就能解决大多数 JavaScript this 指向相关的面试题。