彻底搞懂 JavaScript this:从“谁在打电话”说起
this 是 JS 里最像“口头禅”的东西:写的时候顺手,用的时候迷糊。很多 bug 不是你逻辑错了,而是 this 指向变了。
这篇不背规则表,我们用一个类比讲清楚:this 就是“电话里说的‘我’”——关键不在“我是谁”,而在“谁打来的这通电话”。
1. 为什么 this 这么让人头疼?
因为 this 不是在函数定义时决定的,而是大多在调用时决定:
- 同一个函数,放在不同地方调用,
this可能完全不同 - 一旦函数被“拎出来”单独调用,
this很容易丢
所以理解 this 的关键是:看调用形式(call-site)。
2. 类比:打电话时的“我”是谁?
你在电话里说“我现在很忙”。
- 如果是你妈打来的,“我”是你
- 如果你把手机给朋友接着说,“我”就变成朋友了
- 如果你开了免提让 Siri 代接,“我”可能又不是人类了
JS 也是一样:函数里写的 this,要看“是谁用什么方式调用了它”。
3. 4 条核心规则:this 到底指向谁?
把复杂问题压缩成 4 条优先级规则(从高到低):
规则 1:new 调用(this 指向新对象)
function Person(name) {
this.name = name
}
const p = new Person('Alice')
console.log(p.name) // Alice
new 做了几件事:创建新对象、绑定原型、把 this 指向这个新对象、执行函数、返回对象。
规则 2:显式绑定 call/apply/bind
function say() {
console.log(this.name)
}
const obj = { name: 'Bob' }
say.call(obj) // Bob
say.apply(obj) // Bob
const fn = say.bind(obj)
fn() // Bob
call(thisArg, ...args):立刻调用apply(thisArg, argsArray):立刻调用(参数数组)bind(thisArg, ...args):返回一个“绑定了 this 的新函数”
规则 3:隐式绑定(“点”左边是谁,this 就是谁)
const obj = {
name: 'Cindy',
say() {
console.log(this.name)
},
}
obj.say() // Cindy
注意:这条最容易“丢”。
隐式丢失经典坑
const obj = {
name: 'Cindy',
say() {
console.log(this.name)
},
}
const f = obj.say
f() // this 丢了:非严格模式可能是 window/globalThis,严格模式是 undefined
因为调用点变成了 f(),已经不是 obj.say() 了。
规则 4:默认绑定(没人管时 this 是谁?)
- 严格模式:
this === undefined - 非严格模式:
this === globalThis(浏览器里通常是window)
'use strict'
function foo() {
console.log(this)
}
foo() // undefined
4. 箭头函数:它压根没有自己的 this
箭头函数的 this 不是调用时绑定,而是定义时从外层继承(词法 this)。
const obj = {
name: 'Daisy',
say: function () {
const inner = () => {
console.log(this.name)
}
inner()
},
}
obj.say() // Daisy
如果 inner 是普通函数,inner() 默认绑定会让 this 丢;箭头函数则会“抓住外层的 this”。
箭头函数的一个误区:不能拿来当方法
const obj = {
name: 'Evan',
say: () => {
console.log(this.name)
},
}
obj.say() // 通常是 undefined(因为箭头函数 this 来自定义时的外层,而不是 obj)
结论:对象方法尽量用普通函数;需要继承外层 this 时再用箭头函数。
5. DOM 事件里的 this:你以为是你,其实是元素
button.addEventListener('click', function () {
console.log(this) // button 元素
})
因为监听器回调是由浏览器以“元素作为调用者”触发的。
但如果你写箭头函数:
button.addEventListener('click', () => {
console.log(this) // 外层 this(通常不是 button)
})
想在事件里用元素本身,更通用的方式是用事件对象:
button.addEventListener('click', (e) => {
console.log(e.currentTarget) // 总是当前绑定监听的元素
})
6. 类(class)里最常见的 this 坑:方法被当回调传走
class Counter {
constructor() {
this.count = 0
}
inc() {
this.count++
}
}
const c = new Counter()
setTimeout(c.inc, 0) // this 丢了
修正方式:
A. 构造器里 bind(最传统)
class Counter {
constructor() {
this.count = 0
this.inc = this.inc.bind(this)
}
inc() {
this.count++
}
}
B. 用箭头函数类字段(更常见)
class Counter {
count = 0
inc = () => {
this.count++
}
}
7. 一眼定位 this 的实用步骤(排查法)
遇到 this 不对,按这个顺序看:
- 这次调用是不是
new? - 有没有
call/apply/bind显式指定? - 调用点是不是
obj.fn()这种“点调用”?点左边是谁? - 都不是:默认绑定(严格模式 undefined / 非严格 globalThis)
- 如果是箭头函数:跳过以上规则,直接看它定义时外层的
this
总结:this 不是“我是谁”,是“谁在调用我”
把 this 牢记成一句话:
- 普通函数:
this看调用点 - 箭头函数:
this看定义点(继承外层) - 最容易出事:方法当回调传走导致隐式丢失
- 最稳的解决:bind 或箭头函数类字段 / 在事件里用
currentTarget