JavaScript 中 this 指向的那些坑与解法:从经典面试题到彻底搞懂

59 阅读6分钟

JavaScript 中 this 指向的那些坑与解法:从经典面试题到彻底搞懂

大家好,今天我们来彻底聊聊 JavaScript 中最让人头疼、却又最常考的知识点——this 指向

几乎每个前端面试都会问到 this,为什么?因为它太容易出错,却又无处不在:对象方法、事件回调、定时器、箭头函数……一不小心就掉坑里。

今天就把这些坑全部挖出来,配上代码演示、控制台截图分析和底层原理,一次性帮你彻底搞定 this!


一、经典案例:定时器里的 this 丢了

我们先看一个最常见的面试题:

<!DOCTYPE html>
<html>
<body>
<script>
'use strict'; // 严格模式

var name = 'windowName';

var a = {
    name: 'Cherry',
    func1: function() {
        console.log(this.name);
    },
    func2: function() {
        setTimeout(function() {
            console.log(this);     // ?
            this.func1();          // 会报错!
        }, 1000);
    }
};

a.func2();
</script>
</body>
</html>

运行后,你会发现:

  • 1秒后控制台先打印 undefined(严格模式)或 Window(非严格)
  • 然后直接报错:Cannot read property 'func1' of undefined

为什么?

因为 setTimeout 的回调函数是一个普通函数,当它被执行时,JavaScript 引擎会按照 this 绑定规则决定它的 this 指向。

this 的四种绑定规则(必须背!)

  1. 默认绑定:独立函数调用 → 严格模式下 undefined,非严格下 window
  2. 隐式绑定:obj.func() → this 指向 obj
  3. 显式绑定:call / apply / bind → 强制指定 this
  4. new 绑定:new Func() → this 指向新创建的对象

在 setTimeout 里,回调函数是被窗口(timer)独立调用的,所以走默认绑定 → this 是 window 或 undefined。

这就是为什么外层明明是 a.func2(),里面的 this 却“丢了”。


二、三种经典解决方案对比

面对这个坑,前端前辈们总结了三种解决方案,我们一个个来拆。

方案一:那年那月那 this(var that = this)

func2: function() {
    var that = this; // 把外层的 this 保存起来
    setTimeout(function() {
        console.log(this);  // 还是 window/undefined
        that.func1();       // 用 that 调用 → Cherry
    }, 1000);
}

这是 ES6 之前最常见的写法,靠作用域链保存外层 this。

优点:兼容性好,简单直接
缺点:到处都是 that,代码看着乱;容易忘记写

方案二:显式绑定 —— bind 的“婚约”理论

func2: function() {
    setTimeout(function() {
        console.log(this.name); // Cherry
    }.bind(this), 1000); // 重点在这里!
}

bind(this) 会返回一个新函数,这个新函数的 this 被永久绑定为外层的 this(即 a)。

我喜欢把 bind 比喻成“订婚”:

  • bind 就像下了婚约:以后不管你把这个函数传到哪里(定时器、事件回调),它的 this 永远只认“未婚夫”(绑定的对象)
  • call/apply 是“闪婚”:立刻执行,且指定伴侣

核心区别:bind() 和 call() 的作用完全不一样

方法作用返回值是否立即执行函数
bind()创建一个新的函数,永久绑定 this 为指定对象(这里是 a)返回一个新的绑定后的函数不立即执行
call()立即执行原函数,并指定 this 为指定对象(这里是 a)返回原函数的执行返回值立即执行

记住:bind 返回新函数,不立即执行;call/apply 立即执行。

实验验证:

console.log(a.func1.bind(a));   // 输出一个 bound function(新函数)
console.log(a.func1.call(a));   // 立即输出 Cherry + undefined(返回值)

b220e74b2b4c285a3b09b0a5af2b20a4.png 这就是为什么 bind 打印的是函数对象,

而 call 触发了实际执行(func1 本身没有 return 任何值,所以它的执行返回值是 undefined)。

优点:优雅,this 固定可靠
缺点:每次都要 .bind(this),稍微啰嗦;性能略有开销(创建新函数)

方案三:箭头函数 —— “我没有 this,我抄父亲的”

func2: function() {
    setTimeout(() => {
        console.log(this);  // 就是外层的 a!
        this.func1();       // 完美输出 Cherry
    }, 3000);
}

箭头函数最特殊的地方:它根本没有自己的 this

它会向外层作用域“借” this(词法作用域绑定),层层往上找,直到找到一个有 this 的普通函数或全局。

这也是为什么箭头函数常被说成“没有 this,没有 arguments,没有 prototype”。

实验对比:

const func = () => {
    console.log(this);       // window(全局箭头函数借全局 this)
    console.log(arguments);  // 报错!箭头函数无 arguments
}
func();

优点:最简洁、最现代、最推荐
缺点:不能作为构造函数、不能 bind/call/apply 改变 this


三、易错点大集合(必看!)

易错1:以为对象里的函数 this 永远指向对象 → 大错!

var obj = {
    name: 'obj',
    say: function() {
        console.log(this.name);
    }
};

var fn = obj.say;
fn(); // windowName 或 undefined,不是 obj!

关键看调用方式,不是定义位置。

易错2:严格模式 vs 非严格模式

  • 非严格:默认绑定 → window
  • 严格:默认绑定 → undefined

现代项目基本都用严格模式(模块化自动严格),所以看到 undefined 更常见。

易错3:事件回调中的 this

<button id="btn">点击</button>
<script>
document.getElementById('btn').addEventListener('click', function() {
    console.log(this); // 指向 button 元素!
});
</script>

DOM 事件回调的 this 默认指向触发事件的元素(隐式绑定)。

但如果你用箭头函数:

addEventListener('click', () => {
    console.log(this); // window!因为箭头借了外层
});

根据需求选择。

易错4:嵌套箭头函数的 this

var obj = {
    name: 'outer',
    func: function() {
        console.log(this.name); // outer
        
        const inner = () => {
            console.log(this.name); // 还是 outer!不是 window
        };
        inner();
    }
};
obj.func();

箭头函数借的是定义时外层最近的普通函数的 this,不是调用时。


四、底层逻辑:this 到底是怎么绑定的?

JavaScript 引擎在执行函数时,会根据调用现场决定 this,优先级如下:

  1. new 绑定(最高)
  2. 显式绑定(call/apply/bind)
  3. 隐式绑定(obj.func())
  4. 默认绑定(最低)

箭头函数例外:它在创建时就永久捕获了外层 this,完全不参与运行时的绑定规则。

这就是为什么箭头函数不能被 bind/call 改变 this:

const arrow = () => console.log(this);
const bound = arrow.bind({name: 'fake'});
bound(); // 还是原来捕获的 this,不是 fake

五、实际开发中的最佳实践建议

  1. 优先使用箭头函数处理回调(定时器、Promise、事件等),避免 this 丢失
  2. 类方法中慎用箭头函数(会占用更多内存,不能被继承)
  3. 需要动态 this 时用普通函数 + bind
  4. Vue/React 中注意事件处理器的写法
    • Vue: @click="handleClick" → this 正确
    • React: onClick={this.handleClick.bind(this)}onClick={() => this.handleClick()}
// React 推荐写法(class 组件)
handleClick = () => { // 构造函数里用箭头绑定
    console.log(this);
}

六、总结:一张图记牢 this

调用方式               → this 指向
-------------------------------------------------
obj.func()             → obj             (隐式)
func.call(obj)         → obj             (显式)
new Func()             → 新对象          (new)
func()                 → window/undefined(默认)
=> 箭头函数            → 外层 this       (词法绑定)

记住这五种情况,99%的 this 问题都能解决。


最后的话

this 不是黑魔法,它只是 JavaScript 设计时的一个“动态绑定”机制。理解了调用规则和箭头函数的特殊性,你就会发现:原来一切都有迹可循。

下次再遇到定时器里 this 丢了、事件回调 this 不对、bind 返回函数对象……你都能淡定地说:

“没事,我知道怎么回事。”