谁调用指向谁?从零起步搞定 JavaScript “this 被覆盖”的四大神技

46 阅读6分钟

大家好!欢迎来到前端“避坑”指南。我是你们的思维导图导游。

在 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 不会变? 变量的访问权限在代码编写时(定义时)就确定了。thatfunc2 作用域下的一个普通变量,它会被保存在闭包中,直到定时器执行。
  • 优缺点:这种方法稳如老狗,兼容性极强。缺点是代码不够优雅,满屏幕的 thatself_this 会让新手产生认知负担。

三、 方案二:暴力美学—— callapply

如果你不想等,或者想在特定的场合强行“纠偏”,那么 callapply 就是你的重型武器。

3.1 立即运行的“闪婚”

这两个方法可以手动指定 this 的指向,并立即运行该函数。

JavaScript

var a = {
    name: "Cherry",
    func1: function() {
        console.log(this.name);
    }
}

// 正常调用:a.func1();
// 暴力调用:
a.func1.call(a); // 指定 this 指向 a,并立即运行

3.2 区别在于“行李打包方式”

面试时常问 callapply 的区别,其实非常简单:

  • 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 的不同

  1. 执行时机call 改变指向并立即执行bind 改变指向但返回新函数
  2. 永久性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,它们还有什么不同?请背下这段话:

  1. 没有 this:它的 this 是静态的,捕获自外层执行上下文。
  2. 没有 arguments:箭头函数内部拿不到实参伪数组,请使用 ...args
  3. 不能作为构造函数:因为没有 this 也没有 prototype 属性,所以 new func() 会直接报错。
  4. 无法通过 call/bind 改变:因为它根本没这东西,你传什么给它,它都无动于衷。

六、 总结与最佳实践

面对 this 被覆盖的情况,我们应该如何选择方案?

方案技术原理适用场景
var that = this利用词法作用域和闭包维护老旧代码,或者最简单的逻辑
call / apply显式绑定,立即执行借用其他对象的方法,如 Array.prototype.slice.call(arguments)
bind显式绑定,延迟执行定时器回调、React 类组件的方法绑定
箭头函数静态作用域,不创建上下文现代开发的首选,尤其是异步任务和嵌套函数

避坑总结顺口溜:

谁调用,指向谁;

没人调,找 Window;

想改指向 call 和 bind,

箭头函数看爹是谁!

掌握了这四招,你就能在面试官面前游刃有余地讨论 this 的各种变幻了。


想让我帮你分析更复杂的 this 嵌套(比如箭头函数里套普通函数再套 bind)吗?或者需要我为你生成一张 this 决策流程图?