在 JavaScript 的进阶之路上,this 是一个绕不开的坎。很多人初学时会把它简单理解为“指向当前对象”,但随着代码逻辑复杂化,往往会发现 this 的指向变得扑朔迷离。
今天我结合几个典型的代码场景,深入剖析 this 的设计初衷、运行机制以及判定优先级,希望能帮你彻底理清这个概念。
一、 为什么需要 this?
在讨论 this 之前,我们要先看 词法作用域(Lexical Scope) 。JavaScript 的变量查找是基于声明位置的。
1. 词法作用域的局限
考虑这样一个场景:我们定义了一个对象 bar,里面有一个 printName 方法。
JavaScript
var bar = {
myName: "time.geekbang.com",
printName: function() {
console.log(myName); // 这里会报错或找全局变量
}
}
如果我们在函数内部直接引用 myName,根据作用域链规则,引擎会先在 printName 函数内部找,找不到再往全局找。它不会自动去 bar 这个对象内部找。
如果我们想在对象方法里访问对象自身的属性,在没有 this 的情况下,只能硬编码:console.log(bar.myName)。但这种方式不够灵活,一旦对象改名,代码就会失效。
2. OOP 的诉求
this 的出现,本质上是为了在面向对象编程(OOP)中,让函数能够根据调用环境动态地引用对象成员。它让函数具有了“复用性”,同一个函数可以挂载到不同的对象上,并处理该对象的数据。
二、 this 的设计缺陷与演进
虽然 this 解决了对象方法访问属性的问题,但 JavaScript 早期的设计存在一些“偷懒”行为,导致了逻辑的不一致。
1. 默认绑定的陷阱
当一个函数作为普通函数被调用时(例如 foo()),它的 this 会默认指向全局对象(浏览器中是 window)。
-
设计初衷: 作者可能认为
this必须有个指向,于是顺手给了全局。 -
副作用: 容易导致全局变量污染。如果你在函数内误写了
this.count = 1,你就无意间在window上挂了一个变量。 -
改进方案: * 严格模式 (
'use strict') :在严格模式下,普通函数的this为undefined,这是一种保护机制。- 块级作用域 (
let/const) :使用let声明的全局变量不会挂载到window上,这从侧面降低了this误触全局变量的风险。
- 块级作用域 (
三、 判定 this 指向的四条准则
判定 this 并不看函数在哪里定义,而看函数在哪里被调用(Call-site)。我们可以按优先级从低到高梳理出四条规则:
1. 默认绑定(最低优先级)
独立函数调用。
JavaScript
function foo() { console.log(this); }
foo(); // window (非严格模式)
2. 隐式绑定
当函数作为某个对象的方法被调用时,this 指向该对象。
JavaScript
var myObject = {
name: '极客时间',
showThis: function() { console.log(this.name); }
};
myObject.showThis(); // '极客时间'
隐式丢失问题: 这是开发中最常见的坑。
JavaScript
var foo = myObject.showThis;
foo(); // 结果是 undefined,因为此时变成了“默认绑定”,this 指向 window
这里 foo 仅仅是 showThis 函数的一个引用,调用 foo() 时它并没有通过 myObject 访问,环境变了,this 自然就丢了。
3. 显式绑定 (call, apply, bind)
通过这些方法,我们可以强行指定函数运行时的 this。
- 优点: 极其精准,解决了隐式丢失的问题。
- 场景: 经常用于“借用”其他对象的方法。
4. new 绑定(最高优先级)
当使用 new 关键字调用函数(构造函数)时,JavaScript 引擎实际上做了以下几件事:
- 创建一个空对象。
- 将这个空对象的
__proto__指向构造函数的prototype。 - 将构造函数内部的
this绑定到这个新对象上。 - 执行构造函数代码,并返回该对象。
JavaScript
function Createobj() {
// 模拟 new 的内部逻辑
this.name = "极客时间";
}
var myObject = new Createobj();
这里的 this 最终指向的就是生成的 myObject 实例。
四、 特殊场景:DOM 事件处理
在原生 DOM 操作中,addEventListener 的回调函数如果使用普通函数写法,this 会指向绑定该事件的元素。
JavaScript
link.addEventListener('click', function() {
console.log(this); // 指向 link 这个 DOM 元素
});
这是因为在浏览器内部实现中,调用回调函数时使用了类似 callback.call(currentTarget) 的显式绑定。
五、 总结与最佳实践
理解 this 的关键在于:它是执行上下文(Execution Context)的一部分,是在执行阶段确定的,而非编译阶段。
优先级速记表:
new绑定:this是新创建的对象。- 显式绑定 (
call/apply/bind):this是指定的第一个参数。 - 隐式绑定 (
obj.foo()):this是上下文对象。 - 默认绑定 (
foo()):严格模式下undefined,非严格模式下window。
深度建议:
- 避免多层嵌套的
this:在复杂的异步回调中,this的指向极易混乱。建议使用箭头函数,因为箭头函数没有自己的this,它会捕获外层作用域的this,这让代码行为更符合词法直觉。 - 少用全局
this:尽量开启严格模式,强迫自己写出更安全的代码。
希望这篇文章能帮你建立起对 this 的深度认知。如果你对某个特定场景的指向仍有疑惑,欢迎在评论区贴出代码,我们一起拆解。