大家好!欢迎来到前端“避坑”指南。我是你们的思维导图导游。
在 JavaScript 的世界里,this 就像一个调皮的精灵,它的指向经常让初学者抓狂。你以为它指向 A,结果它跑到了 B。今天,我们就来一场深度大扫除,彻底搞定 “this 指向被覆盖” 的所有解决方案。
一、 核心命题:消失的 this 与它的“案发现场”
在深入解决方案之前,请务必在脑海中刻下这句话:在 JavaScript 的普通函数中,this 的指向不是在定义时确定的,而是在执行(调用)时确定的。
1.1 核心思想:谁调用,指向谁
这是理解 this 的第一准则。
- 作为对象的方法调用:当函数作为对象的方法被调用时,
this指向该对象。 - 孤立调用:当函数脱离了对象直接运行,它的
this就会指向全局(Window/Undefined)。
1.2 现场还原:定时器引发的“惨案”
面试中,最经典的案例莫过于 setTimeout 导致的 this 覆盖。
JavaScript
var a = {
name: "Cherry",
func1: function() {
console.log(this.name);
},
func2: function() {
// 在 func2 内部,此时由 a 调用,this 确实指向 a
console.log("外部 this 指向:", this);
// 坑点出现在这里
setTimeout(function() {
// 噩耗:这里的不一样了!
// setTimeout 的回调函数是独立调用的,它在全局上下文中被激活
// 在非严格模式下,这里的 this 偷偷变成了 Window
console.log("定时器内部 this:", this);
this.func1(); // 报错:this.func1 is not a function
}, 3000);
}
}
a.func2();
为什么会“被覆盖”?
因为 setTimeout 接收的是一个函数定义。当 3000 毫秒过去后,浏览器引擎是直接调用这个匿名函数的,并没有通过 a.xxx() 的方式。于是,原本指向 a 的 this 环境被丢弃,取而代之的是默认的全局环境。
二、 方案一:老派的“分身术”—— var that = this
这是在 ES6 普及之前,最常用也最容易理解的方法。
2.1 词法作用域与作用域链
这个方案的核心其实不是 this 的魔法,而是 JavaScript 的 词法作用域(Lexical Scoping) 。
JavaScript
func2: function() {
// 关键点:保存当前的 this
var that = this;
setTimeout(function() {
// 闭包的神奇之处:
// 虽然这里的 this 指向了 Window,但我们在匿名函数里并没有写 this
// 我们写的是 that。
// 根据“作用域链”,JS 引擎在当前作用域找不到 that,就会向父级寻找
// 于是成功找到了那个指向 a 对象的 that。
that.func1();
}, 3000);
}
2.2 深度解析
- 为什么
that不会变? 变量的访问权限在代码编写时(定义时)就确定了。that是func2作用域下的一个普通变量,它会被保存在闭包中,直到定时器执行。 - 优缺点:这种方法稳如老狗,兼容性极强。缺点是代码不够优雅,满屏幕的
that、self或_this会让新手产生认知负担。
三、 方案二:暴力美学—— call 与 apply
如果你不想等,或者想在特定的场合强行“纠偏”,那么 call 和 apply 就是你的重型武器。
3.1 立即运行的“闪婚”
这两个方法可以手动指定 this 的指向,并立即运行该函数。
JavaScript
var a = {
name: "Cherry",
func1: function() {
console.log(this.name);
}
}
// 正常调用:a.func1();
// 暴力调用:
a.func1.call(a); // 指定 this 指向 a,并立即运行
3.2 区别在于“行李打包方式”
面试时常问 call 和 apply 的区别,其实非常简单:
- call: 传参是一个一个给的,如
func.call(thisArg, arg1, arg2)。 - apply: 传参是打包成数组给的,如
func.apply(thisArg, [argsArray])。
局限性:由于它们会立即执行,所以你不能直接把它们放进 setTimeout 的回调位置。如果你写 setTimeout(this.func1.call(a), 3000),函数会立刻执行,而不是等 3 秒。
四、 方案三:定下“婚约”—— bind
既然 call 太急躁,那有没有一种方法能“预定” this 呢?这就是 bind。
4.1 核心逻辑:指定绑定,以后再执行
bind 是解决 this 覆盖问题的利器。它会创建一个新的函数,这个新函数的 this 被永久锁定为你指定的对象。
JavaScript
const func = a.func1.bind(a); // 指定 this 绑定(a)婚约
// 以后再执行
func();
4.2 完美适配定时器
JavaScript
func2: function() {
// 利用 bind 返回一个新函数的特性
setTimeout(function() {
this.func1();
}.bind(this), 3000);
// 这里的 bind(this) 里的 this 指向 func2 的调用者,即 a
}
面试高频:call 与 bind 的不同
- 执行时机:
call改变指向并立即执行;bind改变指向但返回新函数。 - 永久性:
bind产生的函数是永久绑定的,后续再怎么call都改不掉。
五、 终极杀招:箭头函数 () => {}
如果你嫌 bind 还是太罗嗦,ES6 带来的箭头函数就是为了终结这个混乱局面而生的。
5.1 放弃自我的 this
核心思想:箭头函数没有自己的 this! 它就像一个“透明人”,当你在箭头函数里使用 this 时,它会像看隔壁邻居一样,向外层作用域(父级执行上下文)去寻找。
JavaScript
func2: function() {
// 箭头函数:不产生自己的上下文,没有 this
setTimeout(() => {
// 这里的 this 自动指向了父级作用域(即 func2)里的 this
console.log(this);
this.func1();
}, 3000);
}
5.2 进阶面试:箭头函数与普通函数的四大不同
面试官:除了 this,它们还有什么不同?请背下这段话:
- 没有
this:它的this是静态的,捕获自外层执行上下文。 - 没有
arguments:箭头函数内部拿不到实参伪数组,请使用...args。 - 不能作为构造函数:因为没有
this也没有prototype属性,所以new func()会直接报错。 - 无法通过 call/bind 改变:因为它根本没这东西,你传什么给它,它都无动于衷。
六、 总结与最佳实践
面对 this 被覆盖的情况,我们应该如何选择方案?
| 方案 | 技术原理 | 适用场景 |
|---|---|---|
var that = this | 利用词法作用域和闭包 | 维护老旧代码,或者最简单的逻辑 |
call / apply | 显式绑定,立即执行 | 借用其他对象的方法,如 Array.prototype.slice.call(arguments) |
bind | 显式绑定,延迟执行 | 定时器回调、React 类组件的方法绑定 |
| 箭头函数 | 静态作用域,不创建上下文 | 现代开发的首选,尤其是异步任务和嵌套函数 |
避坑总结顺口溜:
谁调用,指向谁;
没人调,找 Window;
想改指向 call 和 bind,
箭头函数看爹是谁!
掌握了这四招,你就能在面试官面前游刃有余地讨论 this 的各种变幻了。
想让我帮你分析更复杂的 this 嵌套(比如箭头函数里套普通函数再套 bind)吗?或者需要我为你生成一张 this 决策流程图?