谁在调用我?从底层机制拆解 JavaScript this 的“变脸”逻辑

0 阅读5分钟

在 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') :在严格模式下,普通函数的 thisundefined,这是一种保护机制。

    • 块级作用域 (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 引擎实际上做了以下几件事:

  1. 创建一个空对象。
  2. 将这个空对象的 __proto__ 指向构造函数的 prototype
  3. 将构造函数内部的 this 绑定到这个新对象上。
  4. 执行构造函数代码,并返回该对象。

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)的一部分,是在执行阶段确定的,而非编译阶段。

优先级速记表:

  1. new 绑定this 是新创建的对象。
  2. 显式绑定 (call/apply/bind):this 是指定的第一个参数。
  3. 隐式绑定 (obj.foo()):this 是上下文对象。
  4. 默认绑定 (foo()):严格模式下 undefined,非严格模式下 window

深度建议:

  • 避免多层嵌套的 this:在复杂的异步回调中,this 的指向极易混乱。建议使用箭头函数,因为箭头函数没有自己的 this,它会捕获外层作用域的 this,这让代码行为更符合词法直觉。
  • 少用全局 this:尽量开启严格模式,强迫自己写出更安全的代码。

希望这篇文章能帮你建立起对 this 的深度认知。如果你对某个特定场景的指向仍有疑惑,欢迎在评论区贴出代码,我们一起拆解。