JavaScript 中 `this` 的终极解析:从底层机制到实战应用

92 阅读4分钟

引言:为什么 this 总是让人困惑?

你是否曾经遇到过这样的问题:

var obj = {
  name: '极客时间',
  showName: function() {
    console.log(this.name);
  }
};

var fn = obj.showName;
fn(); // 输出什么?undefined?

明明 fn 是从 obj 上拿下来的,为什么 this.name 却不是 '极客时间'

这正是 this 的“魔性”所在。很多人觉得 this 是“动态绑定”,但其实它的行为背后有一套清晰的规则和设计哲学。

今天我们就来系统地拆解 this 的本质,结合多个真实案例,让你真正掌握它!


一、this 是谁?它是如何决定指向的?

核心结论:

this 不是由函数定义的位置决定的,而是由函数被调用的方式(执行上下文)决定的!

换句话说:谁调用我,我就指向谁。

但这不是简单的“对象方法就指向对象”,我们需要分情况讨论。


二、this 指向的五种经典场景

场景1:作为对象的方法被调用 → 指向该对象

let myObj = {
  name: '极客时间',
  showThis: function() {
    console.log(this); // 输出整个 myObj 对象
    this.name = '极客邦';
  }
};

myObj.showThis(); // 此时 this 指向 myObj

结果:this === myObj

原理:myObj.showThis() 调用方式表明 showThismyObj 的方法,所以 this 指向 myObj


场景2:作为普通函数调用 → 指向全局对象(非严格模式),或 undefined(严格模式)

var myObj = {
  name: '极客时间',
  showThis: function() {
    console.log(this);
  }
};

var foo = myObj.showThis;
foo(); // 输出 window(浏览器环境)

这里 foo() 是一个普通函数调用,没有明确的调用者,所以 this 指向全局对象(如 window)。

注意:如果使用 'use strict',此时 this 会是 undefined,防止污染全局变量。

'use strict';
var foo = myObj.showThis;
foo(); // 输出 undefined

场景3:通过 call / apply 显式绑定 this

let bar = { myName: '极客邦' };
function foo() {
  this.myName = '极客时间';
}

foo.apply(bar); // 或 foo.call(bar)
console.log(bar.myName); // 输出:极客时间

applycall 允许我们手动指定 this 的值,这是实现“借用方法”的关键。

小技巧:apply 接受数组参数,call 接受列表参数。


场景4:构造函数中 this 指向新创建的实例

function CreateObj() {
  console.log(this); // 输出一个空对象(新实例)
  this.name = '极客时间';
}

var obj = new CreateObj();
console.log(obj.name); // 极客时间

new 操作符做了三件事:

  1. 创建一个空对象 {}
  2. 设置其原型链为 CreateObj.prototype
  3. 执行 CreateObj.call(新对象),即把 this 绑定到新对象上;
  4. 返回这个对象(除非返回的是另一个对象)。

模拟 new 的过程:

var temObj = {};
CreateObj.call(temObj);
temObj.__proto__ = CreateObj.prototype;
var obj = temObj;

场景5:事件处理函数中的 this

<a href="#" id="link">点击我</a>
<script>
document.getElementById('link').addEventListener('click', function() {
  console.log(this); // 输出:<a> 元素本身
});
</script>

在 DOM 事件监听器中,this 默认指向触发事件的元素。

如果你在事件回调中使用了箭头函数,this 就不会指向 DOM 元素,而是继承外层作用域的 this


三、this 与作用域链的区别

很多人混淆了 this 和变量查找机制(作用域链),下面我们对比一下:

var bar = {
  myName: 'time.geekbang.com',
  printName: function() {
    console.log(myName);        // 查找作用域链:先找局部,再向上
    console.log(bar.myName);    // 显式访问 bar 对象属性
    console.log(this);          // 当前执行上下文
    console.log(this.myName);   // 通过 this 访问属性
  }
};

var myName = '极客邦'; // 全局变量
var _printName = bar.printName;

_printName(); // 输出:'极客邦'(因为 myName 是全局变量)
bar.printName(); // 输出:'time.geekbang.com'
代码查找方式结果
myName作用域链查找找到全局变量 '极客邦'
bar.myName显式访问对象属性'time.geekbang.com'
this.myName依赖 this 指向取决于调用方式

关键点:this 是运行时绑定的,而变量查找是编译期确定的(词法作用域)。


四、this 设计背后的“历史包袱”

JavaScript 早期为了支持面向对象编程,引入了 this 来模拟类的行为。但由于当时还没有 class 语法,只能靠函数 + this 实现 OOP。

设计缺陷:

  • this 的行为不一致,容易造成误解;
  • 普通函数调用时 this 默认指向全局对象,导致全局污染;
  • var 声明的变量会被挂载到 window 上,let/const 才避免这个问题。

改进措施:

  • 使用 'use strict' 防止意外的 this 指向;
  • 使用箭头函数(=>)避免 this 绑定问题;
  • 使用 bindcallapply 显式控制 this
  • ES6+ 推出 class,让 OOP 更清晰。

五、总结:一张图记住 this

调用方式this 指向
对象方法调用 obj.method()obj
普通函数调用 func()全局对象(非严格)或 undefined(严格)
func.call(obj) / func.apply(obj)obj
new func()新创建的对象
DOM 事件监听器触发事件的元素

写在最后

this 是 JavaScript 中最具争议但也最重要的概念之一。它看似简单,实则蕴含着语言设计的深意。掌握它,不仅能帮你写出更稳定的代码,还能更好地理解 JS 的执行机制。