别再让 this 在 setTimeout 里失踪了!三种方法彻底解决

45 阅读6分钟

当 setTimeout 里的 this 消失了,我是这样找回它的

前几天写一个对象方法时,我遇到了一个熟悉又恼人的 bug:在 setTimeout 的回调函数里,this 突然变成了 undefined,导致我无法调用对象自身的其他方法。明明外层的 this 还好好指向着目标对象,怎么一进定时器就“失联”了?

这个问题看似简单,却直指 JavaScript 中 this 绑定机制的核心。为了解决它,我尝试了多种方案——从经典的变量缓存,到显式绑定,再到现代的箭头函数。在这个过程中,我也重新厘清了 callapplybind 的本质区别。今天,就把这次完整的排查和思考过程分享出来。


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 正确指向 athis.func1() 成功输出 "Cherry"

为什么选择 bind?为什么不直接用 callapply

这是关键所在。乍看之下,callapply 也能指定函数内部的 this,比如:

someFn.call(obj);
someFn.apply(obj);

但它们有一个致命限制:会立即执行函数

setTimeout 需要的是一个函数引用,不是执行结果。如果我这样写:

// ❌ 错误!
setTimeout(this.func1.call(a), 3000);

实际发生的是:

  1. 在setTimeout被调用时,call就立即执行了,输出了Cherry,并且返回了undefined,因为func1()并没有返回值
  2. 所以当setTime想要执行回调函数时,回调函数已经变成了undefined,所以它什么也不会做
  3. 看起来控制台确实输出了Cherry,但是这不是在执行回调函数时执行的,而是调用setTimeout时就已经执行完毕了

bind 不同——它不会执行函数,而是返回一个新函数,这个新函数的 this 被永久绑定到指定对象。正因如此,bind 返回的函数可以安全地传给 setTimeout,在 3 秒后才真正执行。

所以结论很明确:

  • 需要延迟执行?用 bind
  • 需要立刻调用?用 callapply

在定时器、事件监听等异步场景中,只有 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 从来不只是“这个”——它关乎上下文、时机,以及你对语言的信任。