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 的四种绑定规则(必须背!)
- 默认绑定:独立函数调用 → 严格模式下 undefined,非严格下 window
- 隐式绑定:obj.func() → this 指向 obj
- 显式绑定:call / apply / bind → 强制指定 this
- 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(返回值)
这就是为什么 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,优先级如下:
- new 绑定(最高)
- 显式绑定(call/apply/bind)
- 隐式绑定(obj.func())
- 默认绑定(最低)
箭头函数例外:它在创建时就永久捕获了外层 this,完全不参与运行时的绑定规则。
这就是为什么箭头函数不能被 bind/call 改变 this:
const arrow = () => console.log(this);
const bound = arrow.bind({name: 'fake'});
bound(); // 还是原来捕获的 this,不是 fake
五、实际开发中的最佳实践建议
- 优先使用箭头函数处理回调(定时器、Promise、事件等),避免 this 丢失
- 类方法中慎用箭头函数(会占用更多内存,不能被继承)
- 需要动态 this 时用普通函数 + bind
- Vue/React 中注意事件处理器的写法:
- Vue:
@click="handleClick"→ this 正确 - React:
onClick={this.handleClick.bind(this)}或onClick={() => this.handleClick()}
- Vue:
// 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 返回函数对象……你都能淡定地说:
“没事,我知道怎么回事。”