this指向

JavaScript中的this指向

核心流程(记忆关键)

记忆口诀:普通函数看调用者,箭头函数看定义处,new 指向新对象

快速判断(一句话总结):

1
2
3
普通函数:谁调用指向谁,没人调用指向全局(严格模式 undefined)
箭头函数:定义时绑定,继承外层 this
构造函数:new 调用指向新创建的实例对象

1. this 指向快速判断(核心规律)

1.1 最常见的 4 种情况

情况1:普通函数调用 → 指向全局对象

1
2
3
4
5
6
7
8
9
10
11
function foo() {
console.log(this) // window(浏览器)或 global(Node.js)
}
foo() // 独立调用,this 指向全局

// 严格模式下
'use strict'
function bar() {
console.log(this) // undefined
}
bar()

情况2:对象方法调用 → 指向调用的对象

1
2
3
4
5
6
7
const obj = {
name: 'Tom',
sayName() {
console.log(this.name) // this 指向 obj
}
}
obj.sayName() // Tom

情况3:构造函数调用 → 指向新创建的实例

1
2
3
4
5
function Person(name) {
this.name = name // this 指向新创建的对象
}
const p = new Person('Tom')
console.log(p.name) // Tom

情况4:箭头函数 → 继承外层 this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const obj = {
name: 'Tom',
sayName: () => {
console.log(this.name) // this 继承外层(全局)
},
sayAge() {
const inner = () => {
console.log(this.name) // this 继承 sayAge 的 this(obj)
}
inner()
}
}
obj.sayName() // undefined(箭头函数 this 指向全局)
obj.sayAge() // Tom(箭头函数继承 sayAge 的 this)

1.2 快速判断口诀

1
2
3
4
5
6
看函数怎么调用:
1. obj.fn() → this 是 obj
2. fn() → this 是 window/undefined
3. new Fn() → this 是新对象
4. fn.call(obj) → this 是 obj
5. 箭头函数 → this 看外层

2. this 绑定规则详解

2.1 默认绑定(独立函数调用)

规则:函数独立调用时,this 指向全局对象(严格模式下是 undefined)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function foo() {
console.log(this)
}
foo() // window(非严格模式)或 undefined(严格模式)

// 常见陷阱:对象方法赋值后调用
const obj = {
name: 'Tom',
sayName() {
console.log(this.name)
}
}
const fn = obj.sayName
fn() // undefined(this 指向全局,全局没有 name)

2.2 隐式绑定(对象方法调用)

规则:作为对象方法调用时,this 指向该对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const obj = {
name: 'Tom',
sayName() {
console.log(this.name) // this 指向 obj
}
}
obj.sayName() // Tom

// 链式调用:this 指向最后调用的对象
const obj1 = {
name: 'Tom',
child: {
name: 'Jerry',
sayName() {
console.log(this.name)
}
}
}
obj1.child.sayName() // Jerry(this 指向 child)

隐式丢失(常见坑):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const obj = {
name: 'Tom',
sayName() {
console.log(this.name)
}
}

// 坑1:赋值给变量
const fn = obj.sayName
fn() // undefined(this 丢失)

// 坑2:作为回调函数
setTimeout(obj.sayName, 1000) // undefined(this 丢失)

// 坑3:传递给其他函数
function execute(callback) {
callback()
}
execute(obj.sayName) // undefined(this 丢失)

2.3 显式绑定(call/apply/bind)

规则:通过 call、apply、bind 显式指定 this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function sayName() {
console.log(this.name)
}

const obj1 = { name: 'Tom' }
const obj2 = { name: 'Jerry' }

// call:立即执行,参数逐个传递
sayName.call(obj1) // Tom

// apply:立即执行,参数数组传递
sayName.apply(obj2) // Jerry

// bind:返回新函数,不立即执行
const boundFn = sayName.bind(obj1)
boundFn() // Tom

三者区别:

1
2
3
4
5
6
7
8
9
function greet(age, city) {
console.log(`${this.name}, ${age}岁, 来自${city}`)
}

const person = { name: 'Tom' }

greet.call(person, 18, '北京') // Tom, 18岁, 来自北京
greet.apply(person, [18, '北京']) // Tom, 18岁, 来自北京
greet.bind(person, 18)('北京') // Tom, 18岁, 来自北京

2.4 new 绑定(构造函数调用)

规则:使用 new 调用时,this 指向新创建的对象

1
2
3
4
5
6
7
function Person(name) {
this.name = name
console.log(this) // Person { name: 'Tom' }
}

const p = new Person('Tom')
console.log(p.name) // Tom

new 的执行过程:

1
2
3
4
1. 创建一个新对象
2. 将新对象的 __proto__ 指向构造函数的 prototype
3. 将构造函数的 this 指向新对象,并执行
4. 如果构造函数返回对象,则返回该对象,否则返回新对象

2.5 箭头函数(特殊规则)

规则:箭头函数没有自己的 this,继承外层作用域的 this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const obj = {
name: 'Tom',
// 错误:箭头函数作为对象方法
sayName: () => {
console.log(this.name) // undefined(this 指向全局)
},
// 正确:普通函数内部使用箭头函数
sayAge() {
const inner = () => {
console.log(this.name) // Tom(继承 sayAge 的 this)
}
inner()
}
}

obj.sayName() // undefined
obj.sayAge() // Tom

箭头函数特点:

  • 没有自己的 this,继承外层作用域
  • this 在定义时确定,不是调用时确定
  • 无法通过 call/apply/bind 改变 this
  • 不能作为构造函数使用(不能 new)

3. this 绑定优先级

优先级从高到低:

1
2
3
4
5
1. new 绑定          → this 指向新对象
2. 显式绑定 → this 指向指定对象
3. 隐式绑定 → this 指向调用对象
4. 默认绑定 → this 指向全局/undefined
5. 箭头函数 → 继承外层 this(不参与优先级)

优先级验证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function foo() {
console.log(this.name)
}

const obj1 = { name: 'Tom', foo }
const obj2 = { name: 'Jerry' }

// 隐式绑定 vs 默认绑定
obj1.foo() // Tom(隐式绑定优先)

// 显式绑定 vs 隐式绑定
obj1.foo.call(obj2) // Jerry(显式绑定优先)

// new 绑定 vs 显式绑定
const boundFoo = foo.bind(obj1)
const instance = new boundFoo() // undefined(new 绑定优先)

4. 经典面试题(高频5题)

题目1:隐式丢失

1
2
3
4
5
6
7
8
const obj = {
name: 'Tom',
sayName() { console.log(this.name) }
}

obj.sayName() // Tom(隐式绑定)
const fn = obj.sayName
fn() // undefined(this丢失)

题目2:箭头函数陷阱

1
2
3
4
5
6
7
8
9
10
11
const obj = {
name: 'Tom',
sayName: () => {
console.log(this.name) // undefined(继承全局this)
},
sayAge() {
return () => {
console.log(this.name) // Tom(继承sayAge的this)
}
}
}

题目3:setTimeout中的this

1
2
3
4
5
6
7
8
9
10
11
12
13
const obj = {
name: 'Tom',
delayedSay() {
setTimeout(function() {
console.log(this.name) // undefined(独立调用)
}, 1000)
},
delayedSayArrow() {
setTimeout(() => {
console.log(this.name) // Tom(箭头函数继承this)
}, 1000)
}
}

题目4:bind只生效一次

1
2
3
4
5
6
7
8
function foo() { console.log(this.name) }

const obj1 = { name: 'Tom' }
const obj2 = { name: 'Jerry' }

const boundFoo = foo.bind(obj1)
boundFoo() // Tom
boundFoo.call(obj2) // Tom(bind无法二次绑定)

题目5:综合题

1
2
3
4
5
6
7
8
9
10
11
12
const obj = {
name: 'Tom',
foo1: function() { console.log(this.name) },
foo2: () => { console.log(this.name) },
foo3: function() {
return () => { console.log(this.name) }
}
}

obj.foo1() // Tom(隐式绑定)
obj.foo2() // undefined(箭头函数指向全局)
obj.foo3()() // Tom(箭头函数继承foo3的this)

5. 手写实现(核心代码)

手写call

1
2
3
4
5
6
7
8
Function.prototype.myCall = function(context, ...args) {
context = context || window
const fn = Symbol('fn')
context[fn] = this
const result = context[fn](...args)
delete context[fn]
return result
}

手写apply

1
2
3
4
5
6
7
8
Function.prototype.myApply = function(context, args = []) {
context = context || window
const fn = Symbol('fn')
context[fn] = this
const result = context[fn](...args)
delete context[fn]
return result
}

手写bind

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Function.prototype.myBind = function(context, ...args1) {
const self = this

const fBound = function(...args2) {
return self.apply(
this instanceof fBound ? this : context,
[...args1, ...args2]
)
}

if (this.prototype) {
fBound.prototype = Object.create(this.prototype)
}

return fBound
}

6. 面试口述版本

Q: 请解释JavaScript中this的指向规则

“JavaScript中的this是函数运行时的上下文对象,指向取决于调用方式。

主要有五种绑定规则,按优先级从高到低:

第一,new绑定。使用new调用时,this指向新创建的对象。

第二,显式绑定。通过call、apply、bind显式指定this,其中bind会返回硬绑定函数,无法再次改变。

第三,隐式绑定。作为对象方法调用时,this指向该对象。要注意隐式丢失问题,比如赋值给变量或作为回调传递时会丢失this。

第四,默认绑定。独立函数调用时,非严格模式指向全局对象,严格模式是undefined。

第五,箭头函数。没有自己的this,继承外层作用域的this,在定义时确定,无法改变。

实际应用中,回调函数容易丢失this,可以用箭头函数或bind解决。快速判断方法是:看调用时有没有点,有点就是点前面的对象,没点看是否用了call/apply/bind,都没有就是默认绑定,箭头函数直接看外层作用域。”


7. 高频追问(6题)

Q1: 箭头函数和普通函数的this有什么区别?

普通函数: this在调用时确定,可以通过call/apply/bind改变,可以作为构造函数。

箭头函数: this在定义时确定并继承外层作用域,无法改变,不能作为构造函数。


Q2: 为什么bind只能生效一次?

bind返回的是一个新函数,内部已经硬绑定了this。再次bind只是在外面包了一层,但内部的this已经固定无法改变。


Q3: 如何解决回调函数中this丢失?

1
2
3
4
5
6
7
8
9
// 方案1:箭头函数
setTimeout(() => obj.sayName(), 1000)

// 方案2:bind
setTimeout(obj.sayName.bind(obj), 1000)

// 方案3:保存this
const self = obj
setTimeout(function() { self.sayName() }, 1000)

Q4: class中的this指向?

1
2
3
4
5
6
7
8
9
10
11
class Person {
sayName() { console.log(this.name) } // 普通方法
sayAge = () => { console.log(this.name) } // 箭头函数
}

const p = new Person('Tom')
const fn1 = p.sayName
fn1() // undefined(this丢失)

const fn2 = p.sayAge
fn2() // Tom(箭头函数this固定)

Q5: 事件处理函数中的this?

1
2
3
4
5
6
7
8
// DOM事件
button.addEventListener('click', function() {
console.log(this) // button元素
})

button.addEventListener('click', () => {
console.log(this) // 外层作用域的this
})

Q6: 严格模式对this有什么影响?

非严格模式下,独立调用的函数this指向全局对象。严格模式下,this是undefined,且不会自动转换为全局对象。


8. 实战经验(3个典型场景)

场景1:React组件中的this丢失

问题: 事件处理函数中this为undefined

解决:

1
2
3
4
5
6
7
8
9
10
11
12
class Button extends React.Component {
// 方案1:箭头函数(推荐)
handleClick = () => {
this.setState({ count: this.state.count + 1 })
}

// 方案2:bind绑定
constructor() {
super()
this.handleClick = this.handleClick.bind(this)
}
}

场景2:定时器中的this

问题: setTimeout回调中this指向全局

解决:

1
2
3
4
5
6
7
8
9
const obj = {
name: 'Tom',
delayedSay() {
// 使用箭头函数
setTimeout(() => {
console.log(this.name) // Tom
}, 1000)
}
}

场景3:数组方法回调中的this

问题: forEach/map等回调中this丢失

解决:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const obj = {
name: 'Tom',
list: [1, 2, 3],
printList() {
// 方案1:箭头函数
this.list.forEach(item => {
console.log(this.name, item)
})

// 方案2:传入thisArg参数
this.list.forEach(function(item) {
console.log(this.name, item)
}, this)
}
}

9. 记忆路线图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
this指向
├── 快速判断:普通函数看调用者,箭头函数看定义处
├── 五种绑定(优先级)
│ ├── new绑定 → 新对象
│ ├── 显式绑定 → call/apply/bind
│ ├── 隐式绑定 → obj.fn()
│ ├── 默认绑定 → window/undefined
│ └── 箭头函数 → 继承外层
├── 常见陷阱
│ ├── 隐式丢失(赋值、回调)
│ └── 箭头函数无法改变this
└── 解决方案
├── 箭头函数
└── bind绑定

10. 总结

核心要点:

  1. this指向取决于调用方式,不是定义方式
  2. 优先级:new > 显式绑定 > 隐式绑定 > 默认绑定
  3. 箭头函数继承外层this,无法改变
  4. 回调函数易丢失this,用箭头函数或bind解决

记忆口诀:

  • 普通函数看调用者,箭头函数看定义处
  • new最优先,bind强绑定
  • 对象方法看点前,独立调用看模式