JS this 的两种绑定模型:词法作用域与动态调用对比解析

作者:很酷cat2026.11.04 08:21浏览量:0

简介:很多开发者习惯用“谁调用指向谁”记忆 this,但遇到回调、嵌套函数或箭头函数时仍会判断失误。本文从执行上下文与内存模型出发,对比词法作用域和 this 的绑定机制,分析普通函数、对象方法、箭头函数及回调场景中 this 的判断方式,并给出选型与排查建议。

JS this 的两种绑定模型:词法作用域与动态调用对比解析

在 JavaScript 中,this 是最容易让人产生误判的关键字之一。它不像词法作用域那样在代码编写阶段就能确定,而是依赖函数实际执行时的调用方式。很多开发者会背诵“谁调用指向谁”,但遇到回调函数、方法提取、箭头函数嵌套等情况时仍然容易出错。

本文将围绕 JavaScript 中 词法作用域 与 this 动态绑定 这两种机制展开对比,分析它们的设计目标、确定时机、内存模型、常见误区和适用场景,帮助读者从执行上下文和内存视角理解 this 的判断逻辑。

一、为什么需要在词法作用域之外引入 this

1.1 词法作用域解决“变量从哪里来”

JavaScript 采用词法作用域,也就是函数在定义时就已经确定了外层变量环境。编译器在解析代码时会建立作用域链,函数内部访问变量时如果自身环境找不到,就会沿着 outer 指针逐层向外查找。

这种机制适用于普通变量访问,例如:

  1. let name = "outer";
  2. function printName() {
  3. let name = "inner";
  4. console.log(name); // 输出 inner
  5. }

上例中,console.log 使用的 name 由 printName 内部环境决定,与函数在哪里调用无关。

1.2 this 解决“当前对象是谁”

但词法作用域无法解决对象方法需要访问“当前调用对象”的问题。请看下面这个典型场景:

  1. const objA = {
  2. name: "A",
  3. show() {
  4. console.log(this.name);
  5. },
  6. };
  7. const objB = {
  8. name: "B",
  9. show: objA.show,
  10. };
  11. objA.show(); // 输出 A
  12. objB.show(); // 输出 B

同一个 show 函数被两个对象复用,如果只依赖词法作用域,show 内部的 name 无法根据调用对象动态变化。this 的存在,让函数可以在复用同一套代码时,根据调用者动态访问对应对象的数据。

从设计动机看:

  • 词法作用域解决的是“变量声明在哪里”;
  • this解决的是“当前复用代码的接收对象是谁”。

两者不是替代关系,而是面向不同问题域的补充机制。

二、核心差异:静态确定与动态确定

2.1 确定时机不同

这是理解 this 最关键的对比维度。

维度 词法作用域 this 绑定
确定阶段 编译阶段 执行阶段
依据信息 函数声明位置 函数调用方式
是否受调用位置影响 否 是
典型指针 outer 链 this 指针
查找逻辑 逐层向外查找变量 根据调用者绑定对象

换句话说:

  • 词法作用域看“出生地”;
  • this 看“调用现场”。

2.2 执行上下文中的体现

当一个函数被调用时,JavaScript 引擎会创建对应的执行上下文。执行上下文中既包含词法环境和变量环境,也包含 this 值的确定。

词法环境和变量环境在编译阶段已经基本构建完成,outer 指针也由函数声明位置决定。而 this 的绑定则必须等函数实际被调用时才能确定。这也是为什么同一个函数被不同方式调用时,词法作用域不变,但 this 可能完全不同。

2.3 示意性代码对比

  1. let name = "global";
  2. const obj = {
  3. name: "obj",
  4. show() {
  5. console.log(this.name);
  6. },
  7. getName: function () {
  8. return function () {
  9. console.log(this.name);
  10. };
  11. },
  12. };
  13. const extracted = obj.show;
  14. extracted(); // 普通函数调用,this 通常不指向 obj
  15. const innerFn = obj.getName();
  16. innerFn(); // 同样属于普通函数调用,this 通常不指向 obj

在上面代码中,无论是 extracted 还是 innerFn,它们的词法作用域没有发生任何变化,但 this 绑定已经因为调用方式不同而改变。

三、this 绑定规则的分类与对比

在实际开发中,this 的判断可以从函数调用类型入手进行归纳。

3.1 普通函数调用

当函数以独立方式调用时,this 不指向某个特定对象。在非严格模式下,this 通常指向全局对象;在严格模式下,this 为 undefined。

  1. function test() {
  2. "use strict";
  3. console.log(this);
  4. }
  5. test(); // undefined

这种场景最容易出现在方法提取、回调函数等情况下。

3.2 对象方法调用

当函数作为对象的属性被调用时,this 指向调用该方法的对象。

  1. const user = {
  2. name: "Alice",
  3. showName() {
  4. console.log(this.name);
  5. },
  6. };
  7. user.showName(); // this 指向 user

注意这里的关键不是“函数定义在对象里”,而是“函数通过对象属性访问的方式被调用”。

3.3 显式绑定

通过 call、apply、bind 等方式可以显式指定 this 的指向。显式绑定的优先级通常高于对象方法调用。

  1. function greet() {
  2. console.log(this.name);
  3. }
  4. const person = { name: "Bob" };
  5. greet.call(person); // 输出 Bob

3.4 构造函数调用

当函数通过 new 关键字调用时,函数内部的 this 指向新创建的对象。

  1. function Person(name) {
  2. this.name = name;
  3. }
  4. const p = new Person("Carol");
  5. console.log(p.name); // 输出 Carol

3.5 箭头函数的特殊行为

箭头函数不创建自己的 this 绑定,而是沿用外层作用域的 this。这意味着箭头函数的 this 更像词法作用域:它由定义位置的外层函数 this 决定。

  1. const obj = {
  2. name: "obj",
  3. show() {
  4. const inner = () => {
  5. console.log(this.name);
  6. };
  7. inner();
  8. },
  9. };
  10. obj.show(); // 输出 obj

箭头函数和普通函数在 this 行为上的差异,可以总结如下:

函数类型 是否拥有自己的 this this 确定方式 适合回调场景
普通函数 是 调用时动态绑定 需要动态 this 时
箭头函数 否 沿用定义位置外层 this 希望保持外层 this 时

四、高频误区与排查思路

4.1 方法被提取后丢失原绑定

这是实际项目中最常见的问题之一。

  1. const obj = {
  2. name: "obj",
  3. show() {
  4. console.log(this.name);
  5. },
  6. };
  7. const fn = obj.show;
  8. fn(); // this 不指向 obj

虽然 fn 来自 obj.show,但调用时已经不再是 obj.show() 的形式,因此 this 会丢失。

解决方案通常有三种:

  1. 使用 bind 显式绑定:const fn = obj.show.bind(obj);
  2. 使用箭头函数包裹:const fn = () => obj.show();
  3. 调用时仍保持对象调用形式。

4.2 回调函数中的 this

在事件回调、定时器、数据处理回调等场景中,回调函数通常以普通函数方式被调用,因此在非严格模式下容易指向全局对象或 undefined。

  1. const obj = {
  2. name: "obj",
  3. register() {
  4. setTimeout(function () {
  5. console.log(this.name);
  6. }, 100);
  7. },
  8. };
  9. obj.register(); // this 不指向 obj

如果希望回调中继续使用 obj 的上下文,可以考虑:

  1. register() {
  2. setTimeout(() => {
  3. console.log(this.name);
  4. }, 100);
  5. }

箭头函数在这里的作用不是“改变了 this 的语义”,而是因为箭头函数不绑定自己的 this,从而自然沿用了 register 方法中的 this。

4.3 嵌套函数内部 this 丢失

普通函数内部再定义普通函数并调用时,内层函数的 this 不会自动继承外层函数。

  1. const obj = {
  2. name: "obj",
  3. show() {
  4. function inner() {
  5. console.log(this.name);
  6. }
  7. inner();
  8. },
  9. };
  10. obj.show(); // 内层函数 this 不指向 obj

这是一个高频误区:开发者往往认为内层函数“定义在 show 内部”,就应该沿用 show 的 this。但 this 不受定义位置决定,而受调用方式决定。

五、从内存模型视角理解 this

5.1 对象方法是函数,不是对象的一部分

在 JavaScript 中,函数是独立的对象。当函数被定义为对象的属性时,对象属性只是保存了对函数对象的引用。

以下示意性结构可以辅助理解:

  1. obj 对象
  2. ├── name 属性
  3. ├── show 属性 ——→ 函数对象(独立存在)

因此,obj.show() 这种调用方式会向函数传递一个隐式的“当前对象”信息,即 this。而 const fn = obj.show; fn(); 这种调用方式没有通过对象属性访问,因此不会传递该信息。

5.2 词法作用域由函数对象创建时决定

与 this 不同,词法作用域信息保存在函数对象内部。当函数被创建时,引擎会记录其外层环境引用。之后无论函数被赋值给哪个变量、作为哪个对象的方法调用,这个作用域链都不会改变。

这也是为什么以下代码不受调用位置影响:

  1. let a = "global";
  2. function outer() {
  3. let a = "outer";
  4. return function inner() {
  5. console.log(a);
  6. };
  7. }
  8. const fn = outer();
  9. fn(); // 输出 outer

inner 函数访问的 a 由 inner 定义时的词法环境决定,而不是由 fn() 的调用位置决定。

六、两种机制在真实开发中的适用场景

6.1 需要动态 this 的场景

当需要复用同一套函数逻辑操作不同对象时,应该使用普通函数并依赖 this 的动态绑定。

典型场景包括:

  • 对象方法;
  • 构造函数;
  • 类实例方法;
  • 需要根据调用对象动态读取数据的通用工具函数。

例如:

  1. function fullName() {
  2. return `${this.firstName} ${this.lastName}`;
  3. }
  4. const user1 = { firstName: "A", lastName: "B", fullName };
  5. const user2 = { firstName: "C", lastName: "D", fullName };
  6. user1.fullName(); // A B
  7. user2.fullName(); // C D

6.2 需要固定上下文的场景

当回调函数需要保持定义位置的外层 this 时,箭头函数是更合适的选择。它可以让函数“记住”定义时的 this 环境,减少显式绑定代码。

典型场景包括:

  • 事件监听器中的辅助函数;
  • 定时器回调;
  • 数据处理回调;
  • 框架组件方法内部封装异步操作。
  1. class Counter {
  2. constructor() {
  3. this.count = 0;
  4. }
  5. start() {
  6. setInterval(() => {
  7. this.count++;
  8. console.log(this.count);
  9. }, 1000);
  10. }
  11. }

上例中,箭头函数使得 this 始终指向 Counter 实例,不依赖 setInterval 的调用方式。

七、常见对比总结

对比项 词法作用域 this 动态绑定
核心问题 变量从哪个环境查找 当前调用对象是谁
确定时间 编译阶段 执行阶段
决定因素 定义位置 调用方式
主要影响 变量访问、闭包 对象属性访问、方法复用
常见指针 outer 引用 this 指针
传递方式 函数创建时记录 调用时隐式传递
箭头函数行为 正常参与词法作用域 不绑定自己的 this,沿用外层
是否可显式控制 否 可通过 call、apply、bind 调整
排查难度 低,位置明确 较高,需关注调用链路

八、迁移到清晰可维护代码的注意事项

如果项目中 this 相关问题频繁出现,可以考虑从以下方面调整:

8.1 减少对 this 的隐式依赖

对于不依赖动态对象的纯函数,尽量写成普通函数,不依赖 this。这样函数的行为完全由参数决定,测试和复用都更简单。

8.2 方法提取时显式绑定或使用箭头函数

当需要把对象方法作为回调传递时,应明确绑定上下文,避免隐式丢失。

  1. const handler = obj.handle.bind(obj);
  2. // 或
  3. const handler = (...args) => obj.handle(...args);

8.3 类实例方法谨慎传递

在使用类实例方法时,也要注意方法被解构或传递后可能出现 this 丢失。可以在构造函数中提前绑定,或者用箭头函数定义类字段,但需要注意不同环境对类字段语法的支持情况。

8.4 团队内统一 this 使用规范

建议在团队内部形成统一约定:

  • 何时使用普通函数;
  • 何时使用箭头函数;
  • 方法传递时是否强制显式绑定;
  • 是否允许在回调中直接使用 this。

这类规范可以减少因个人编码习惯差异带来的隐性风险。

九、总结

this 与词法作用域解决的是不同层次的问题:词法作用域负责“变量从哪里来”,this 负责“当前对象是谁”。前者在编译阶段由定义位置决定,后者在执行阶段由调用方式决定。

“谁调用指向谁”这个口诀本身没有错,但它只是对对象方法调用场景的简化总结,无法覆盖普通函数调用、显式绑定、new 调用和箭头函数等更复杂的情况。更准确的判断方式应该是:

  1. 先判断是否为箭头函数:如果是,this 由外层作用域决定;
  2. 再判断是否使用 new:如果是,this 指向新创建的实例对象;
  3. 再判断是否有 call、apply、bind 等显式绑定:如果有,按显式指定为准;
  4. 再判断是否为对象方法调用:如果是,this 指向调用对象;
  5. 否则按普通函数调用处理。

理解 this 的关键不在于记忆规则,而在于理解 JavaScript 函数作为独立对象、this 作为调用时隐式传递的信息这一设计逻辑。掌握了这一点,大部分 this 判断问题都可以从调用现场得到答案。