JavaScript 中 `this` 的设计与深入理解:从自由变量查找、作用域链到执行上下文

54 阅读8分钟

在 JavaScript 编程中,this 是一个看似简单却极易引发困惑的关键字。它不像其他语言中的 this 那样总是指向类的实例,而是由函数的调用方式动态决定,这使得它的行为常常出人意料。

本文将结合你提供的代码示例和核心概念(如自由变量查找、作用域链、Lexical Scope、执行上下文等),系统性地讲解 JavaScript 中 this 的设计原理、常见陷阱以及最佳实践,帮助开发者真正掌握这一关键机制。


一、前置知识:自由变量查找与词法作用域(Lexical Scope) 🧠

在讨论 this 之前,我们需要先明确另一个重要概念 —— 变量的查找机制

1. 编译阶段与作用域链

JavaScript 虽然是解释型语言,但它有“编译阶段”的概念。在代码执行前,引擎会进行词法分析,确定变量的作用域。

  • 词法作用域(Lexical Scope) :函数的作用域是在其声明时就确定的,而不是运行时。
  • 作用域链(Scope Chain) :当访问一个变量时,JS 引擎首先在当前作用域查找,若未找到,则沿着外层作用域逐级向上查找,直到全局作用域为止。
  • 自由变量(Free Variable) :指在函数内部使用,但既不是参数也不是局部变量,而是来自外层作用域的变量。
let myName = '极客时间';

function foo() {
    console.log(myName); // myName 是自由变量,从外层作用域获取
}

这里的 myName 就是 foo 函数中的自由变量,它的值通过词法作用域链找到。

注意:变量查找依赖的是“声明位置”,而 this 的指向则完全取决于“调用方式”——这是二者本质区别!


二、this 到底是什么?为何如此特殊? 🤔

1. this 的定义

this 是一个关键字,表示“当前执行上下文中的上下文对象”。可以通俗理解为:

“谁调用了这个函数,this 就指向谁。”

但这只是一个粗略的说法,实际情况更为复杂。

执行上下文.png

这是执行上下文中this的邻居

红色代表在执行阶段动态确认,蓝色代表编译阶段静态确定

2. this 的设计问题:由调用方式决定

与其他语言不同,JavaScript 的 this 不是在函数定义或类构造时绑定的,而是在运行时根据调用方式动态绑定

这种设计源于早期 JS 没有 class 关键字,需要借助函数模拟面向对象编程。为了支持方法能访问所属对象的属性,引入了 this

然而这也带来了混乱:

  • 同一个函数,被不同方式调用,this 可能完全不同。
  • 在普通函数中使用 this 往往没有意义,但却默认指向全局对象(window),容易造成污染。

三、this 的五种典型指向情况 🔍

我们可以通过以下五种场景来全面掌握 this 的指向规则。


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

var bar = {
  myName: "time.geekbang.com",
  printName: function () {
    console.log(this);        // bar 对象
    console.log(this.myName); // "time.geekbang.com"
  }
};

bar.printName(); // 正常调用,this 指向 bar

结论:当函数作为某个对象的属性被调用时,this 指向该对象。


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

var myName = "极客帮";

function foo() {
  console.log(this);        // 非严格模式下为 window;严格模式下为 undefined
  console.log(this.myName); // 非严格模式可能输出 "极客帮"(挂载在 window 上)
}

foo(); // 相当于 window.foo()

⚠️ 问题所在

  • 在非严格模式下,this 自动指向 window
  • 所有 var 声明的全局变量都会成为 window 的属性,导致潜在的全局污染
  • 使用 let/const 声明的变量不会挂载到 window,因此即使 this 指向 window 也无法访问它们。

💡 解决方案:使用 'use strict';

'use strict';
function strictFoo() {
  console.log(this); // undefined
}
strictFoo(); // 不再指向 window,避免误操作

建议:始终启用严格模式以防止意外的全局 this 绑定。


场景 3:作为构造函数被调用 → 指向新创建的实例 🏗️

function CreateObj() {
  this.name = "极客时间";
  console.log(this); // 新创建的对象实例
}

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

🔧 构造函数的工作流程:

  1. 创建一个空对象;
  2. 将构造函数的 prototype 赋给该对象的 __proto__
  3. this 指向这个新对象并执行构造函数体;
  4. 返回该对象(除非显式返回其他对象)。

手动模拟 new 操作符:

function CreateObj() {
  this.name = "极客时间";
}

// 手动实现 new 的效果
var temObj = {};
CreateObj.call(temObj);           // 显式绑定 this
temObj.__proto__ = CreateObj.prototype;
var myObj = temObj;

console.log(myObj.name); // "极客时间"

场景 4:通过 call / apply / bind 显式指定 → 指向传入的对象 🎛️

这三个方法允许我们手动控制 this 的指向。

callapply

立即执行函数,并指定 this

var bar = {
  myName: '极客帮',
  test1: 1
};

function setMyName() {
  this.myName = '极客时间';
}

setMyName.call(bar);   // this 指向 bar
console.log(bar.myName); // "极客时间"

// apply 参数传递方式不同(第二个参数是数组)
setMyName.apply(bar, []);

bind

返回一个新的函数,永久绑定 this

var boundFunc = setMyName.bind(bar);
boundFunc();
console.log(bar.myName); // "极客时间"

💡 bind 常用于解决回调函数中 this 丢失的问题。


场景 5:事件处理函数 → 指向绑定的 DOM 元素 🖱️

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

✅ 浏览器自动将事件处理函数的 this 绑定为触发事件的 DOM 元素。

⚠️ 注意闭包中的陷阱:

document.getElementById("link").addEventListener("click", () => {
  console.log(this); // window(箭头函数无自己的 this)
});

所以事件监听推荐使用普通函数而非箭头函数,除非你明确知道后果。


四、经典陷阱:方法赋值后 this 丢失 💥

var myObj = {
  name: "极客时间",
  showThis: function() {
    console.log(this); // 期望是 myObj
  }
};

var foo = myObj.showThis; // 只是引用函数,脱离了对象
foo(); // 输出 window(非严格模式)或 undefined(严格模式)

🔍 分析:

  • foo() 是作为普通函数调用,不是 myObj.foo()
  • 即便函数原本属于某个对象,一旦独立调用,this 就不再指向原对象。

✅ 解决方案:

  • 使用 call / apply / bind 显式绑定:

    foo.call(myObj); // 正确绑定
    
  • 或者提前绑定:

    var boundShow = myObj.showThis.bind(myObj);
    boundShow(); // 始终指向 myObj
    

五、深入理解:执行上下文(Execution Context)视角下的 this 🧩

JavaScript 执行函数时会创建一个执行上下文(Execution Context) ,包含三个部分:

  1. 变量环境(Variable Environment)
  2. 词法环境(Lexical Environment)
  3. this 绑定(This Binding)

其中,this 的绑定发生在函数被调用时,依据如下优先级顺序:

调用方式this 指向
new Func()新创建的实例
func.call(obj) / func.apply(obj) / func.bind(obj)显式指定的对象
obj.method()obj
func()全局对象(非严格)或 undefined(严格)
箭头函数继承外层作用域的 this

📌 this 绑定优先级(高 → 低):

  1. new
  2. bind / call / apply
  3. 对象调用(隐式绑定)
  4. 默认绑定(普通函数调用)
  5. 箭头函数(词法继承)

六、箭头函数对 this 的影响 🏹

ES6 引入的箭头函数改变了游戏规则:

  • 箭头函数没有自己的 this
  • 它的 this 继承自外层函数或全局作用域的 this
  • 不能用 call / apply / bind 改变其 this
var name = 'window';

var obj = {
  name: 'obj',
  normalFunc: function() {
    console.log(this.name); // 'obj'
    
    setTimeout(function() {
      console.log(this.name); // 'window'(普通函数)
    }, 100);

    setTimeout(() => {
      console.log(this.name); // 'obj'(箭头函数继承外层 this)
    }, 100);
  }
};

obj.normalFunc();

✅ 箭头函数非常适合用于回调,避免 this 丢失。


七、如何优雅地拿到 time.geekbang.com? 🎯

回顾最初的问题:

如何在函数内正确访问 bar.myName(即 "time.geekbang.com")?

看这段代码:

var bar = {
  myName: "time.geekbang.com",
  printName: function() {
    console.log(myName);       // ❌ 报错!找不到 myName
    console.log(bar.myName);   // ✅ 正确:直接访问对象属性
    console.log(this.myName);  // ✅ 只有当 this 指向 bar 时才有效
  }
};

❗ 错误做法:console.log(myName)

  • myName 并非局部变量或参数,也不是外层作用域变量(全局没有 myName),所以会报错。
  • 这里的 myName 并不是自由变量,而是试图访问不存在的标识符。

✅ 正确做法:

  1. 使用 this.myName —— 当且仅当函数作为 bar 的方法调用时有效。
  2. 使用 bar.myName —— 更稳定,但耦合性强。
  3. 使用闭包保存引用(更高级技巧):
var bar = (function() {
  const myName = "time.geekbang.com";
  return {
    printName: function() {
      console.log(myName); // 通过闭包访问
    }
  };
})();

八、总结:this 设计的利与弊 ⚖️

优点缺点
支持灵活的函数复用(如借用方法)行为难以预测,学习成本高
支持动态上下文切换(OOP 模拟)普通函数中 this 指向全局易造成污染
可通过 call/apply/bind 精确控制方法赋值后 this 丢失常见 bug
事件处理天然绑定 DOM 元素箭头函数虽好,但也改变了传统逻辑

🧠 最佳实践建议

  1. 使用 'use strict' 避免无意中修改 window
  2. 多用 bind 或箭头函数防止 this 丢失。
  3. 尽量避免依赖 this,可改用参数传递数据。
  4. 使用 ES6 Class 替代构造函数,语义更清晰。
  5. 调试时打印 this,确认当前上下文。

结语 🌟

JavaScript 的 this 虽然设计上存在争议,但它也是 JS 灵活性的体现之一。理解 this 的核心在于记住一句话:

this 不看函数怎么写,只看函数怎么调!

结合词法作用域、执行上下文和调用方式,我们才能真正驾驭 this,写出健壮、可维护的代码。

掌握 this,是迈向高级 JavaScript 开发者的必经之路。🚀