当 setTimeout 里的 this 消失了,我是这样找回它的
前几天写一个对象方法时,我遇到了一个熟悉又恼人的 bug:在 setTimeout 的回调函数里,this 突然变成了 undefined,导致我无法调用对象自身的其他方法。明明外层的 this 还好好指向着目标对象,怎么一进定时器就“失联”了?
这个问题看似简单,却直指 JavaScript 中 this 绑定机制的核心。为了解决它,我尝试了多种方案——从经典的变量缓存,到显式绑定,再到现代的箭头函数。在这个过程中,我也重新厘清了 call、apply 和 bind 的本质区别。今天,就把这次完整的排查和思考过程分享出来。
1. 问题代码:this 在定时器中“消失”了
这是我最初写的代码:
'use strict';
var name = "windowName"; // 全局变量
var func1 = function(){
console.log("func1");
}
var a = {
name: "Cherry",
func1: function(){
console.log(this.name);
},
func2: function(){
console.log(this);
setTimeout(function(){
console.log(this);
this.func1();
}, 3000)
}
}
a.func2();
运行后,控制台输出:
{ name: 'Cherry', func1: [Function], func2: [Function] }
undefined
TypeError: Cannot read property 'func1' of undefined
通过控制台输出我们能够看到,外层的this确实正确的指向了当前对象a,但是回调函数中的this却是undefined,原因很明显,因为回调函数是被作为普通函数调用的,所以它的this自然而然的指向了全局,在严格模式下自然也就是undefined,但是这不是我们的本意,我们希望回调函数中的this能够正确的指向我们想要的对象,而不是被覆盖,那么有什么办法呢?
2. 方案一:使用 .bind() 显式绑定 this
我首先想到的是用 Function.prototype.bind() 来固定回调函数中的 this:
setTimeout(function(){
console.log(this);
this.func1();
}.bind(a), 3000)
运行后,一切正常:this 正确指向 a,this.func1() 成功输出 "Cherry"。
为什么选择 bind?为什么不直接用 call 或 apply?
这是关键所在。乍看之下,call 和 apply 也能指定函数内部的 this,比如:
someFn.call(obj);
someFn.apply(obj);
但它们有一个致命限制:会立即执行函数。
而 setTimeout 需要的是一个函数引用,不是执行结果。如果我这样写:
// ❌ 错误!
setTimeout(this.func1.call(a), 3000);
实际发生的是:
- 在setTimeout被调用时,call就立即执行了,输出了Cherry,并且返回了undefined,因为func1()并没有返回值
- 所以当setTime想要执行回调函数时,回调函数已经变成了undefined,所以它什么也不会做
- 看起来控制台确实输出了Cherry,但是这不是在执行回调函数时执行的,而是调用setTimeout时就已经执行完毕了
而 bind 不同——它不会执行函数,而是返回一个新函数,这个新函数的 this 被永久绑定到指定对象。正因如此,bind 返回的函数可以安全地传给 setTimeout,在 3 秒后才真正执行。
所以结论很明确:
- 需要延迟执行?用
bind。 - 需要立刻调用?用
call或apply。
在定时器、事件监听等异步场景中,只有 bind 是合适的。
3. 方案二:通过变量缓存 this
在还没广泛使用 ES6 之前,我常用一个更“原始”的办法:把 this 存到一个局部变量里。
var that = this;
setTimeout(function(){
console.log(this); // 仍是 undefined
that.func1(); // ✅ 但 that 指向 a!
}, 3000)
虽然回调函数内部的 this 依然不对,但通过闭包,它能访问外层作用域的 that,从而间接调用到正确的方法。
这种方法可靠、兼容性好,唯一的缺点是需要额外声明变量,代码略显冗余。但在不支持箭头函数的老项目中,它依然是一个稳妥的选择。
4. 方案三:使用箭头函数(现代推荐)
现在我更倾向于用 ES6 的箭头函数来解决这类问题:
setTimeout(() => {
console.log(this);
this.func1();
}, 3000)
箭头函数没有自己的 this 绑定,它会自动从外层词法作用域继承 this。在我调用 a.func2() 时,外层的 this 是对象 a,因此箭头函数内部的 this 也指向 a,完美解决了上下文丢失的问题。
更重要的是,箭头函数不会创建自己的函数执行上下文(execution context) 。这意味着:
- 它没有自己的
this; - 它没有自己的
arguments对象; - 它不能被用作构造函数(即不能通过
new调用); - 它也没有
super或new.target等绑定。
例如,如果你在箭头函数中尝试访问 arguments,会发现它要么引用外层函数的 arguments,要么在严格模式下直接报错(如果外层也没有):
function outer() {
return () => {
console.log(arguments); // 引用的是 outer 的 arguments!
};
}
outer("hello")(); // 输出: ["hello"]
这种“透明”的上下文特性,正是箭头函数在处理回调、事件处理器或定时器时如此可靠的原因——它不会干扰你原本的 this 指向,也不会引入额外的作用域噪音。
这种方式简洁、直观,不需要手动绑定,也不需要中间变量,是我目前在现代项目中的首选方案。当然,前提是你明确知道:箭头函数不适合用作需要动态 this 的对象方法。但在回调场景中,它几乎总是最佳选择。
5. 三种方案对比总结
| 方法 | 原理 | 是否立即执行 | 是否适合 setTimeout | 推荐度 |
|---|---|---|---|---|
call / apply | 临时指定 this 并立即调用 | ✅ 是 | ❌ 否 | ⚠️ 不适用 |
bind | 创建一个 this 固定的新函数 | ❌ 否 | ✅ 是 | ✅ 推荐(兼容场景) |
变量缓存 (that) | 利用闭包保存对 this 的引用 | — | ✅ 是 | ⚠️ 可用(旧环境) |
| 箭头函数 | 自动继承外层 this | — | ✅ 是 | ✅✅ 强烈推荐(ES6+) |
结语
this 的指向问题,表面看是个小细节,实则牵涉到 JavaScript 函数调用机制、作用域和执行上下文的核心逻辑。正是这样一个“明明写了却找不到”的 undefined,让我重新审视了回调函数中的上下文传递方式。
现在回头看,无论是用 .bind() 显式绑定、用变量缓存 this,还是直接拥抱箭头函数的词法继承,每种方案背后都体现了对语言特性的不同理解和取舍。而在异步场景中,最关键的一条原则始终不变:如果你希望函数在未来某个时刻以正确的上下文运行,就绝不能在当下就把它执行掉。
希望我的这次踩坑与填坑经历,能为你省下几分钟调试时间,甚至避免一个线上 bug。毕竟,在 JavaScript 的世界里,this 从来不只是“这个”——它关乎上下文、时机,以及你对语言的信任。