第 20 章

this:5种绑定规则、优先级与 ResolveThisBinding

this 的值由函数的调用方式决定,不由定义位置决定——箭头函数是唯一的例外

this 是 JavaScript 中最常令开发者困惑的机制,核心原因是一个反直觉的设计:this 不是在函数定义时确定的,而是在函数被调用时根据调用方式确定的。唯一的例外是箭头函数——它没有自己的 this,而是在定义时从外层词法环境继承 this,无论怎么调用都不会改变。

规范在执行上下文层面通过 ThisBinding 状态组件来存储 this 值,通过 ResolveThisBinding 抽象操作来查找当前有效的 this。掌握 this 的 5 条绑定规则和它们的优先级,能够消解 90% 以上的 this 相关困惑。

this 的动态性设计来源于 JavaScript 的早期定位:一门主要用于处理网页 DOM 事件的脚本语言。事件处理函数需要知道"是哪个元素触发了这个事件",于是把触发元素作为 this 传给处理函数,这是隐式绑定规则的最初设计场景。随着 JavaScript 被用于越来越复杂的应用,this 的动态性开始变成负担,因为在类方法、回调、高阶函数等场景里,this 会在不知不觉中改变。ES2015 引入箭头函数,提供了一个词法绑定的 this,让函数能够安全地捕获定义时的 this,而不必担心调用方式的影响。


🔹 Level 1 · 你需要知道的

5 种绑定规则

规则 1:默认绑定(Default Binding)

默认绑定是"兜底"规则,当其他 4 条规则都不适用时,走默认绑定。判断方式:函数调用时,调用表达式里没有 .(对象.方法),没有 call/apply/bind,也没有 new,就是默认绑定。

function foo() {
  console.log(this);
}

foo();               // window(非严格)或 undefined(严格)

规则 2:隐式绑定(Implicit Binding)

隐式绑定是最直觉化的规则:通过对象调用方法时,this 自动绑定为那个对象。规范的描述是:如果函数调用表达式的 Reference Record 的 [[Base]] 是一个对象,那个对象就成为 this。直觉地说,就是调用时 . 左边的对象:

const obj = {
  name: 'Alice',
  greet() {
    console.log(this.name);
  }
};

obj.greet(); // 'Alice'(this = obj)

规则 3:显式绑定(Explicit Binding)

使用 callapplybind 显式指定 this

function greet(greeting) {
  console.log(`${greeting}, ${this.name}`);
}

const alice = { name: 'Alice' };
const bob   = { name: 'Bob' };

greet.call(alice, 'Hello');    // 'Hello, Alice'
greet.apply(bob, ['Hi']);      // 'Hi, Bob'
const boundGreet = greet.bind(alice);
boundGreet('Hey');             // 'Hey, Alice'

规则 4:new 绑定(New Binding)

new 操作符做了四件事:1)创建一个新的空对象;2)将这个对象的 [[Prototype]] 设置为构造函数的 prototype 属性;3)以这个新对象作为 this 调用构造函数;4)如果构造函数返回一个对象,以它为结果,否则返回第 1 步创建的那个新对象。this 就是第 1 步创建的那个新对象:

function Person(name) {
  this.name = name; // this 是新对象
}

const alice = new Person('Alice');
console.log(alice.name); // 'Alice'

规则 5:箭头函数(Lexical this)

箭头函数是 ES2015 引入的函数简写形式,但它不只是语法糖——它有一个关键的语义差异:没有自己的 this。箭头函数的 FunctionEnvironmentRecord 的 [[ThisBindingStatus]]'lexical',意味着它完全不参与 this 绑定,ResolveThisBinding 会穿过箭头函数的 ER 继续向外层查找。这使得箭头函数总是使用它定义时所在作用域this,且无论用什么方式调用,都不能改变这个 this

const obj = {
  name: 'Alice',
  greet() {
    // 这是普通方法,this = obj
    const arrow = () => {
      console.log(this.name); // 继承外层 greet 的 this(= obj)
    };
    arrow();
    arrow.call({ name: 'Bob' }); // 仍然是 'Alice'!call 对箭头函数无效
  }
};

obj.greet(); // 'Alice', 'Alice'

5 条规则的优先级(从高到低)

优先级 规则 示例
1(最高) new 绑定 new Foo()
2 显式绑定(bind > call/apply) foo.call(obj)
3 隐式绑定 obj.foo()
4(最低) 默认绑定 foo()
特殊 箭头函数(词法绑定,不参与上述规则) () => this

🔸 Level 2 · 它是怎么运行的

5 种绑定规则的调用方式与 this 对应示意

┌────────────────────────────────────────────────────────────────────────┐
│                  5 种调用方式与 this 值的映射关系                        │
│                                                                        │
│  调用方式                  this 值                                      │
│  ──────────────────────────────────────────────────────────────────    │
│  foo()                  → undefined (严格) / globalThis (非严格)        │
│                                                                        │
│  obj.foo()              → obj (调用时 . 左边的对象)                     │
│  obj.a.b.foo()          → obj.a.b (最近的 . 左边的对象)                 │
│                                                                        │
│  foo.call(thisArg)      → thisArg (非严格下 null/undefined → global)   │
│  foo.apply(thisArg)     → thisArg                                      │
│  foo.bind(thisArg)()    → thisArg (new 可以覆盖)                        │
│                                                                        │
│  new foo()              → 新创建的对象 (忽略 bind 绑定的 this)           │
│                                                                        │
│  箭头函数 () => ...      → 定义时外层词法环境的 this                     │
│                          (不受任何调用方式影响)                        │
└────────────────────────────────────────────────────────────────────────┘

隐式绑定丢失

隐式绑定丢失是 this 相关 bug 中最常见的类型,几乎每个 JavaScript 开发者都遇到过。理解它的根本原因,就能快速识别所有的丢失场景:隐式绑定的生效条件是"调用表达式的形式是 对象.方法()",一旦方法被赋值给变量,Reference Record 的 [[Base]] 就从对象变成了 EnvironmentRecord(变量所在的作用域),隐式绑定的条件不再满足,退化为默认绑定。

隐式绑定只在通过对象调用时生效。把方法赋值给变量后再调用,隐式绑定丢失,变为默认绑定:

const obj = {
  name: 'Alice',
  greet() { console.log(this.name); }
};

obj.greet();        // 'Alice'(隐式绑定)

const fn = obj.greet;
fn();               // undefined(默认绑定,严格模式)/ window.name(非严格)

// 常见场景:解构赋值
const { greet } = obj;
greet();            // 同样丢失

// 常见场景:回调函数
setTimeout(obj.greet, 0); // 丢失,this 是 window(非严格)

原因fn() 是直接调用,不是通过对象调用。fn.[[ReferencedName]] 的 Base 不是 obj,所以不触发隐式绑定规则。

bind 的实现原理与 new 覆盖 bind 的行为

bind 是显式绑定中最"持久"的一种:它返回一个新函数(规范称为 BoundFunctionExoticObject),这个新函数永久记住了绑定的 this 和预填的参数,不管怎么调用它,this 都是绑定好的值——除了一种情况:new 调用。当使用 new 来调用一个 bind 产生的函数时,new 的优先级更高,[[BoundThis]] 会被忽略,this 变为新创建的对象。

bind 返回一个新函数(BoundFunctionExoticObject),内部有 [[BoundThis]] 槽存储绑定的 this,有 [[BoundTargetFunction]] 槽存储原函数,有 [[BoundArguments]] 槽存储预填的参数。

// 模拟 bind 的行为(简化版)
Function.prototype.myBind = function(thisArg, ...presetArgs) {
  const originalFn = this;
  return function bound(...callArgs) {
    // 如果通过 new 调用,this 是新对象,忽略 thisArg
    if (new.target) {
      return new originalFn(...presetArgs, ...callArgs);
    }
    return originalFn.call(thisArg, ...presetArgs, ...callArgs);
  };
};

new 覆盖 bind

function Person(name) {
  this.name = name;
}

const BoundPerson = Person.bind({ name: 'Bound' });

const p = new BoundPerson('Alice');
// new 操作忽略 bind 绑定的 { name: 'Bound' }
// this 是新创建的对象,name = 'Alice'
console.log(p.name); // 'Alice'

规范依据:调用 BoundFunctionExoticObject 的 [[Construct]] 时,它使用目标函数的 [[Construct]],传入 [[BoundArguments]] + arguments但不传入 [[BoundThis]]。新对象的 this[[Construct]] 内部创建,不受 bind 影响。

箭头函数的词法 this:不是"继承",是"没有"

箭头函数的 this 行为经常被描述为"继承外层的 this",这个说法虽然在效果上是正确的,但在机制上是不准确的。准确的说法是:箭头函数没有自己的 this 绑定this 关键字在箭头函数里被求值时,引擎不会在箭头函数的 FunctionEnvironmentRecord 里找到 ThisBinding,于是沿 [[OuterEnv]] 链向外查找,找到第一个有 ThisBinding 的 ER(通常是外层的普通函数 ER 或 Global ER),返回那个 ER 里的 [[ThisValue]]。这不是"继承"——没有任何值被"传递"或"复制",只是查找路径穿过了箭头函数。

理解这个机制的实际价值:当你嵌套多层箭头函数时,this 始终"穿透"到第一个非箭头函数的作用域,不管嵌套多深:

function outer() {
  // outer 的 this = 调用 outer 时的 this
  const inner = () => {
    // 箭头函数,无自己的 this,穿透到 outer
    const innermost = () => {
      // 再一层箭头函数,继续穿透
      console.log(this); // 与 outer 的 this 相同,不管嵌套多深
    };
    innermost();
  };
  inner();
}

const obj = { name: 'test', outer };
obj.outer(); // this 是 obj,三层函数里的 this 都是 obj

箭头函数对应的 FunctionEnvironmentRecord 的 [[ThisBindingStatus]]'lexical',意味着它没有自己的 ThisBinding。当对箭头函数的 ER 调用 HasThisBinding() 时,返回 falseResolveThisBinding 算法在当前 ER 没有 ThisBinding 时,会继续向外层 ER 查找,直到找到一个有 ThisBinding 的 ER。

箭头函数调用时的 ResolveThisBinding 路径:

  箭头函数的 ER (HasThisBinding = false)
       ↓ 向外查找
  外层函数的 ER (FunctionEnvironmentRecord, HasThisBinding = true)
       ↓
  返回外层函数的 [[ThisValue]]

这就是为什么 call/apply/bind 对箭头函数无效——不是因为"忽略了 this",而是箭头函数根本没有 ThisBinding,ResolveThisBinding 不会停在箭头函数的 ER,自然也无法被修改。

call(null) 和 call(undefined) 的行为差异

function foo() {
  console.log(this);
}

// 非严格模式
foo.call(null);      // window(浏览器)/ global(Node.js)
foo.call(undefined); // 同上

// 严格模式
'use strict';
function bar() {
  console.log(this);
}
bar.call(null);      // null
bar.call(undefined); // undefined

规范(OrdinaryCallBindThis 算法):

这是为了向后兼容:ES3 时代 call(null) 被广泛用作"不关心 this"的写法,ES5 在严格模式下修正了这个行为,但非严格模式必须保持原有语义。


🔺 Level 3 · 规范怎么定义的

规范 9.4.1.3:ResolveThisBinding

this 关键字在规范里的求值是通过 ResolveThisBinding() 这个抽象操作来实现的。当 JavaScript 代码里出现 this 表达式时,引擎调用这个操作来确定当前有效的 this 值。理解这个操作的工作方式,就理解了 this 机制在规范层面的完整实现。

ResolveThisBinding() 是一个抽象操作,在 this 表达式被求值时调用:

ResolveThisBinding():
1. 取当前运行的 EC 的 LexicalEnvironment 作 envRec
2. 断言:envRec 必须是 FunctionEnvironmentRecord 或 GlobalEnvironmentRecord 或 ModuleEnvironmentRecord
3. 返回 envRec.GetThisBinding()

而 FunctionEnvironmentRecord.GetThisBinding() 的规范定义:

FunctionEnvironmentRecord.GetThisBinding():
1. 如果 [[ThisBindingStatus]] 是 'lexical' → 抛出 ReferenceError
   (实际上不会直接到这里,因为箭头函数的 ER 的 HasThisBinding() 返回 false,
   会先向外层 ER 查找)
2. 如果 [[ThisBindingStatus]] 是 'uninitialized' → 抛出 ReferenceError
   (用 super() 调用前 this 未初始化的情况,class 派生类构造函数)
3. 返回 [[ThisValue]]

更精确地,this 关键字的求值规范(Section 13.2.2 The this Keyword):

1. 返回 ? ResolveThisBinding()

但规范中对 this 的实际处理是通过"运行中的 EC 的 LexicalEnvironment 链"来解析的——当 HasThisBinding 返回 false 时(箭头函数),继续向外的 ER 查找,直到找到返回 true 的 ER,再调用 GetThisBinding。

规范 10.4.4:箭头函数的 ThisMode

箭头函数通过 OrdinaryFunctionCreate 创建时,thisMode 参数是 'lexical'

ArrowFunction 的创建:
  OrdinaryFunctionCreate(..., thisMode = 'lexical', ...)
  → F.[[ThisMode]] = 'lexical'

当箭头函数被调用,PrepareForOrdinaryCall 创建新 FunctionEnvironmentRecord 时:

NewFunctionEnvironment(F, newTarget):
  如果 F.[[ThisMode]] 是 'lexical':
    env.[[ThisBindingStatus]] = 'lexical'
  (不设置 [[ThisValue]],因为箭头函数没有 this 绑定)

后续 OrdinaryCallBindThis 步骤:

OrdinaryCallBindThis(F, calleeContext, thisArgument):
  1. 如果 F.[[ThisMode]] 是 'lexical' → 返回(什么都不做)
  // 箭头函数跳过了 this 绑定的整个流程!
  2. ...(对于非箭头函数,根据 strict/global 模式绑定 this)

规范:OrdinaryCallBindThis 的完整逻辑

OrdinaryCallBindThis(F, calleeContext, thisArgument) 是普通函数调用时绑定 this 的规范操作:

1. 取 F.[[ThisMode]] 为 thisMode
2. 如果 thisMode 是 'lexical' → 返回(箭头函数,不绑定)
3. 取 calleeContext 的 LexicalEnvironment 为 localEnv
4. 如果 thisMode 是 'strict':
   thisValue = thisArgument
5. 否则(非严格模式):
   a. 如果 thisArgument 是 undefined 或 null:
      thisValue = 当前 Realm 的全局对象
   b. 否则如果 thisArgument 不是对象:
      thisValue = ToObject(thisArgument)  ← 基本类型包装(boxing)
   c. 否则:
      thisValue = thisArgument
6. 调用 localEnv.BindThisValue(thisValue)
7. 返回 thisValue

步骤 5b 解释了一个常见的现象:在非严格模式下调用 foo.call(42)thisNumber(42)(包装对象),不是 42(原始值)。严格模式下 this 就是 42

类方法里 this 的差异:原型方法 vs 实例箭头函数

在 React class 组件和其他面向对象代码里,this 的绑定问题最常在"把方法传给事件处理器"时出现。理解两种定义方式的本质区别,能帮助你做出正确的设计选择:

原型方法:定义在 Class.prototype 上,所有实例共享同一个函数对象。调用时 this 取决于调用方式——直接通过实例调用时(instance.method())是实例,但作为回调传递时会丢失绑定。

实例箭头函数:在构造函数里用 this.method = () => {} 定义,每个实例有自己的函数对象。this 是词法绑定,永远是创建它的那个实例,不会丢失绑定。代价是每个实例独立的函数对象带来额外内存开销,且无法被覆写(在子类里 super.method 无法访问箭头函数,因为它不在原型链上)。

class Counter {
  constructor() {
    this.count = 0;
    
    // 方式 1:实例箭头函数(在构造函数里定义)
    // 每个实例有自己的函数对象,this 是词法绑定(始终是实例)
    this.increment = () => {
      this.count++;
    };
  }
  
  // 方式 2:原型方法(定义在 prototype 上)
  // 所有实例共享一个函数对象,this 取决于调用方式
  decrement() {
    this.count--;
  }
}

const c = new Counter();
const { increment, decrement } = c;

increment(); // 正常:箭头函数,this 始终是 c
decrement(); // this = undefined(严格模式下,隐式绑定丢失)

内存影响:

如果类有大量实例,实例箭头函数会造成显著的内存开销。正确做法是用原型方法,在需要固定 this 时手动 bind(通常只在事件处理器里)。


💎 Level 4 · 边界与陷阱

陷阱 1:解构赋值导致隐式绑定丢失

解构赋值是 ES2015 的重要特性,提供了从对象中提取属性的简洁语法。但很多开发者没有意识到,解构方法时会同时丢失隐式绑定。原因很直接:const { getName } = obj 等价于 const getName = obj.getName,只是赋值了函数引用,没有保留调用上下文。后续 getName() 是直接调用,不是通过 obj 调用,隐式绑定条件不满足,退化为默认绑定。

const obj = {
  name: 'Alice',
  getName() {
    return this.name;
  }
};

// 直接调用:正常
console.log(obj.getName()); // 'Alice'

// 解构后调用:丢失
const { getName } = obj;
console.log(getName()); // undefined(严格模式)/ window.name(非严格)

// 同样的问题:传给回调
[1, 2, 3].forEach(obj.getName); // 丢失

// 解决方案 1:bind
const boundGetName = obj.getName.bind(obj);
console.log(boundGetName()); // 'Alice'

// 解决方案 2:箭头函数包裹
console.log([1].map(() => obj.getName())[0]); // 'Alice'

// 解决方案 3:在类里用箭头函数定义方法
class Person {
  constructor(name) {
    this.name = name;
    this.getName = () => this.name; // 箭头函数,this 永远是实例
  }
}

陷阱 2:setTimeout 导致隐式绑定丢失

setTimeoutsetInterval 接收函数引用作为参数,当它们延迟调用这个函数时,调用方式是"直接调用"(没有通过对象),所以隐式绑定丢失,退化为默认绑定。这个问题在定时器、异步回调、事件监听器等几乎所有"延迟调用"的场景里都会出现,是仅次于解构赋值的最常见 this 丢失场景:

const timer = {
  seconds: 0,
  start() {
    // 错误:this 丢失
    setInterval(this.tick, 1000);
  },
  tick() {
    this.seconds++; // 这里 this 是 window(非严格)或 undefined(严格)
    console.log(this.seconds);
  }
};

timer.start(); // NaN 或 TypeError

// 修复方式 1:箭头函数包裹
const timerFixed1 = {
  seconds: 0,
  start() {
    setInterval(() => this.tick(), 1000); // 箭头函数,this 是 timerFixed1
  },
  tick() {
    this.seconds++;
    console.log(this.seconds); // 1, 2, 3...
  }
};

// 修复方式 2:bind
const timerFixed2 = {
  seconds: 0,
  start() {
    setInterval(this.tick.bind(this), 1000);
  },
  tick() {
    this.seconds++;
    console.log(this.seconds);
  }
};

陷阱 3:new 覆盖 bind 绑定的 this(bind 对 new 无效)

new 覆盖 bindthis 这个行为,是 bind 的一个专门设计的特性,而不是意外的边界情况。规范这样设计的原因是:bind 最常用的场景是创建"部分应用"函数(预填部分参数),用于柯里化或依赖注入。如果允许在创建"预填了参数的构造函数"后用 new 来实例化它,new 应该正常工作,创建一个新对象,而不是被 bindthis 干扰。[[BoundArguments]]new 调用时仍然有效,这让"绑定部分参数"的功能完全正常工作:

function Vehicle(type) {
  this.type = type;
}

const CarFactory = Vehicle.bind({ type: 'bound' });

// 普通调用:bind 生效
const car1 = CarFactory('SUV');
// 注意:没有 new,这是普通函数调用,this 是 bind 的 { type: 'bound' }
// 实际上这会修改 { type: 'bound' } 的 type 属性,返回 undefined
// 更有意义的用法:

function Point(x, y) {
  this.x = x;
  this.y = y;
}

const PointFromOrigin = Point.bind(null, 0, 0); // 预设 x=0, y=0

const p1 = PointFromOrigin.call({ x: 999 }); // call 也被 bind 覆盖了?不是!
// 等等——bind 返回的函数,再 call 还是用 bind 的 this
// 但 new 会覆盖 bind 的 this

const p2 = new PointFromOrigin(); // this 是新创建的对象,不是 null
console.log(p2); // Point { x: 0, y: 0 }(new 覆盖了 bind 的 null this)

// 验证 new 优先级高于 bind
function Foo() {
  console.log(this === global || this === window ? 'global' : 'new object');
  console.log(this.constructor === Foo); // true(new 创建的对象)
}

const BoundFoo = Foo.bind({ id: 1 });
BoundFoo();      // 普通调用:this 是 { id: 1 }(bind 生效)
new BoundFoo();  // new 调用:this 是新对象(bind 的 this 被忽略)

陷阱 4:类方法里 this 的实例箭头函数 vs 原型方法内存差异

class WithArrow {
  constructor() {
    // 实例箭头函数:每个实例独立的函数对象
    this.onClick = (event) => {
      console.log(this.id); // this 始终是实例
    };
    this.id = Math.random();
  }
}

class WithMethod {
  constructor() {
    this.id = Math.random();
  }
  
  // 原型方法:所有实例共享一个函数对象
  onClick(event) {
    console.log(this.id); // this 取决于调用方式
  }
}

// 内存对比(创建 10000 个实例)
const arrows = Array.from({ length: 10000 }, () => new WithArrow());
const methods = Array.from({ length: 10000 }, () => new WithMethod());

// arrows:10000 个 onClick 函数对象(每个约 100 字节)= ~1MB 额外开销
// methods:1 个 onClick 函数对象共享 = 几乎无额外开销

在 React class 组件的时代,this.handleClick = this.handleClick.bind(this) 在构造函数里 bind 是标准做法,但这也有同样的问题(每个实例一个绑定函数)。函数式组件 + Hooks 通过 useCallback 缓解了这个问题。

陷阱 5:call(null) 和 call(undefined) 在非严格模式的历史遗留陷阱

call(null) 在老代码里被广泛用作"我不需要 this,只需要传参数"的惯用写法,比如 Array.prototype.slice.call(arguments, 0)arguments 转为数组。这在严格模式下和预期完全一致(this 就是 null),但在非严格模式下,null 和 undefined 会被自动替换为全局对象,这在某些情况下会导致意外的全局变量访问或修改:

function getThis() {
  return this;
}

// 非严格模式
console.log(getThis.call(null));      // Window(浏览器)
console.log(getThis.call(undefined)); // Window(浏览器)
console.log(getThis.call(0));         // Number {0}(ToObject 包装)
console.log(getThis.call('hello'));   // String {'hello'}(ToObject 包装)

// 严格模式
'use strict';
function getThisStrict() {
  return this;
}
console.log(getThisStrict.call(null));      // null
console.log(getThisStrict.call(undefined)); // undefined
console.log(getThisStrict.call(0));         // 0(不包装,原始值)
console.log(getThisStrict.call('hello'));   // 'hello'(不包装)

这个差异在使用 .call(null) 作为"不需要 this"的场景时特别重要:

// 老代码:用 call(null) 表示"不关心 this"
[1, 2, 3].forEach(function(x) {
  console.log(x);
}, null); // 第二个参数是 thisArg,传 null → 非严格模式下仍是 window

// 现代写法:箭头函数,不需要 this,不受影响
[1, 2, 3].forEach(x => console.log(x));

实践中影响最大的场景:老代码里大量存在 Array.prototype.slice.call(arguments, 0) 这类写法,call 的第一个参数是 arguments(对象),这在非严格模式和严格模式下行为一致(都是 arguments 对象作为 this),但如果改成 call(null)call(undefined) 就会有差异。


附:class 中 this 的深层行为与 super 的关系

class 语法让 this 的行为更规范化,但也引入了一些需要理解的细节,特别是在派生类(继承)场景里:

class Animal {
  constructor(name) {
    this.name = name;
    // 注意:派生类重写了构造函数时,
    // 基类构造函数里的 this 已经是最终对象(派生类实例)
    console.log(this.constructor.name); // 'Dog'(如果 Dog extends Animal 且 super() 调用了这里)
  }
  
  speak() {
    console.log(`${this.name} makes a sound`);
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    // 派生类构造函数里,在调用 super() 之前,this 处于"未初始化"状态
    // 此时访问 this 会报错:ReferenceError: Must call super constructor
    // super(name); // 必须先调用
    super(name);
    // super() 调用后,this 才可以使用
    this.breed = breed;
  }
  
  speak() {
    // super.speak() 调用父类方法
    // 规范:super.speak 产生的 Reference Record 的 [[Base]] 是 Animal.prototype
    // 但 GetThisValue 返回的是当前 Dog 实例([[ThisValue]] = this)
    // 所以在 Animal.speak() 里,this.name 访问的是 Dog 实例的 name
    super.speak();
    console.log(`${this.name} barks`);
  }
}

const dog = new Dog('Rex', 'Labrador');
dog.speak(); // "Rex makes a sound" + "Rex barks"

派生类构造函数里 thissuper() 之前处于"未初始化"状态,这是规范的专门设计:FunctionEnvironmentRecord 的 [[ThisBindingStatus]] 在调用 super() 之前是 'uninitialized',任何 GetThisBinding() 调用都会抛出 ReferenceErrorsuper() 调用完成后,[[ThisBindingStatus]] 变为 'initialized'this 才可以使用。这确保了父类构造函数有机会在子类代码访问 this 之前完成初始化工作。

附:全局上下文和模块顶层的 this

全局上下文和 ESM 模块顶层的 this 是两个经常被误解的特殊场景:

// 浏览器全局上下文(script 标签里)
console.log(this === window); // true(Global EC 的 ThisBinding = globalThis)

// 浏览器 ESM 模块顶层(type="module")
console.log(this); // undefined!
// 模块顶层的 this 是 undefined,不是 window
// 规范:ModuleEnvironmentRecord.HasThisBinding() 返回 true
// 但 GetThisBinding() 返回 undefined(模块没有自己的 this)

// Node.js 脚本(CommonJS)
console.log(this); // {}(空对象,不是 global)
// Node.js 把每个 CommonJS 模块包在一个函数里执行:
// (function(exports, require, module, __filename, __dirname) { ... })
// 这个包装函数里的 this 是 module.exports(初始是空对象 {})
// 执行完后 module.exports 就是最终的导出值

// Node.js ESM 模块顶层
// console.log(this); // undefined(与浏览器 ESM 相同)

这个差异解释了为什么"在模块里检测全局对象要用 globalThis 而不是 this":在 ESM 模块里 thisundefined,但 globalThis 始终指向全局对象(浏览器里是 window,Node.js 里是 global)。globalThis 是 ES2020 引入的,专门为了在任何上下文里(脚本、模块、Worker)都能安全地访问全局对象而设计的。


小结

  1. this 的值由函数的调用方式决定,共有 4 条运行时规则(优先级从高到低):new 绑定(this 是新对象)、显式绑定(call/apply/bind 指定的值)、隐式绑定(obj.method() 中的 obj)、默认绑定(严格模式下 undefined,非严格模式下全局对象);箭头函数是词法绑定,不参与这 4 条规则。

  2. 隐式绑定丢失是最常见的 this bug:把方法赋值给变量、解构赋值、或传给 setTimeout/事件监听器,都会导致隐式绑定丢失,变成默认绑定;解决方案是箭头函数包裹或 bind

  3. new 的优先级高于 bindnew BoundFn() 中,[[BoundThis]] 被忽略,this 是新创建的对象;bind 预填的参数([[BoundArguments]])仍然有效。

  4. 箭头函数的本质不是"继承外层 this",而是"没有自己的 ThisBinding":FunctionEnvironmentRecord 的 [[ThisBindingStatus]]'lexical'HasThisBinding() 返回 false,ResolveThisBinding 向外层 ER 查找直到找到有 ThisBinding 的 ER 为止;call/apply/bind 对箭头函数调用 OrdinaryCallBindThis 时,发现 [[ThisMode]] === 'lexical' 就直接返回,不绑定任何 this。

  5. 非严格模式下,call(null)call(undefined) 会把 this 替换为全局对象;call(42) 会把 42 经 ToObject 包装为 Number {42} 对象;严格模式下传什么就是什么,没有自动转换。

本章评分
4.8  / 5  (10 评分)

💬 留言讨论