this 指向原理、箭头函数语义与 call/apply/bind
2025-12-09 16:24
在执行上下文与作用域一文中,我们已经提到:词法环境会在执行上下文创建时确定 this 的绑定规则;而在原型、原型链与继承我们也分析过构造函数在通过 new 调用时,会将 this 指向新创建的实例对象。所以在这篇文章中我们来了解一下 this。
什么是 this?
在 JavaScript 中,this 并不是一个普通的变量,它不参与作用域链查找,也不是在函数定义时就被确定的。很多初学者会误以为 this 指向“函数所在的对象”或“函数定义的位置”,但这些理解在实际运行中往往并不成立。
从执行模型的角度来看,this 是执行上下文的一部分。当一个函数被调用时,JavaScript 引擎会为其创建对应的执行上下文,并在这一阶段根据函数的调用方式,为 this 绑定一个具体的值。因此,this 的指向并不取决于函数写在哪里,而是取决于函数是如何被调用的。
这也意味着,同一个函数在不同的调用场景下,this 可能指向完全不同的对象。例如,作为普通函数调用、作为对象的方法调用、通过 call / apply / bind 调用,或者通过 new 关键字调用,都会触发不同的 this 绑定规则。
this 是在什么时候被确定的?
this 的绑定,并不是在函数定义阶段完成的,而是在函数执行上下文创建时才被确定。
当 JavaScript 引擎准备执行一个函数时,会为该函数创建一个新的执行上下文。在这个过程中,引擎需要完成三件事情:
- 建立词法环境(变量、函数声明等)
- 建立作用域链
- 确定本次执行上下文中的 this 值
需要注意的是,执行上下文只负责为
this预留位置,而this的具体指向,则取决于函数的调用形式。也就是说,this并不是函数自身的属性,而是一次“调用行为”的结果。
因此,即使是同一个函数,在不同的调用方式下,每一次执行都会创建新的执行上下文,而其中的 this 也可能指向不同的对象。
this 的绑定规则
在 JavaScript 中,this 的指向并不是随意的,而是遵循一套明确且可推导的绑定规则。函数在被调用时,JavaScript 引擎会根据调用表达式的形式,在执行上下文创建阶段为 this 绑定具体的值。
这些规则并不相互独立,而是存在明确的适用场景和优先级。理解它们的关键,不在于记住结论,而在于能够根据调用方式推导出 this 的指向。
默认绑定:普通函数调用
当函数以普通形式调用时,this 指向全局对象(浏览器环境下是 window,Node.js 环境下是 globalThis)。
JAVASCRIPTfunction foo() { console.log(this); } foo(); // window 或 globalThis
在非严格模式下,默认绑定会将 this 指向全局对象(浏览器环境中为 window,Node.js 中为 global);而在严格模式下,this 则为 undefined。
需要注意的是,默认绑定与函数是否定义在对象内部无关,只与调用时是否存在调用者对象有关。
隐式绑定:作为对象的方法调用
当函数通过对象属性的形式被调用时,会触发隐式绑定规则,此时 this 指向调用该方法的对象。
JAVASCRIPTconst obj = { foo() { console.log(this); }, }; obj.foo(); // this === obj /// 需要特别注意的是,隐式绑定并不具备传递性: const fn = obj.foo; fn(); // 默认绑定,this 丢失
这里的关键不在于函数“属于谁”,而在于调用表达式中点号左侧的对象是谁。一旦函数引用脱离了原始对象,调用时就不再满足隐式绑定条件。
显式绑定:call / apply / bind
当函数通过 call / apply / bind 方法被调用时,会触发显式绑定规则,此时 this 指向第一个参数指定的对象。
JAVASCRIPTfunction foo() { console.log(this); } foo.call(obj); foo.apply(obj);
call 与 apply 的区别仅在于参数传递形式,而它们的共同点是:在函数调用的当次执行中直接修改 this 的指向。
bind 则不同,它不会立即执行函数,而是返回一个永久绑定了 this 的新函数:
JAVASCRIPTconst boundFoo = foo.bind(obj); boundFoo(); // this === obj
一旦 this 被 bind 固定,后续再通过 call 或 apply 也无法修改其指向。
为什么要这样设计?
因为 bind 方法的主要作用是创建一个新函数,该函数的 this 值永久绑定到指定对象,而不会受到后续调用的影响。这个新函数包含三样东西:
- 函数体:与原函数相同
this值:绑定到指定对象- 参数列表:与原函数相同 可以抽象成:
JAVASCRIPTboundFoo = function (...args) { // 这不是规范原文,但语义是等价的。 return foo.call(obj, ...args); };
那为什么 call / apply 对它无效是因为 boundFoo 执行时根本不会调用传入时的 this,而是使用调用时固定绑定的对象。可以理解为 bind 返回的函数已经没有再绑定 this 阶段了。
new 绑定:构造函数调用
当函数通过 new 关键字调用时,会触发 new 绑定规则,此时 this 指向新创建的实例对象。
JAVASCRIPTfunction Foo() { this.name = "Foo"; } const foo = new Foo(); console.log(foo.name); // Foo
绑定规则的优先级
当函数被调用时,JavaScript 引擎会根据以下优先级顺序确定 this 的绑定:
new绑定call / apply / bind绑定- 隐式绑定
- 默认绑定
理解这一优先级,可以帮助我们在复杂调用场景中快速判断
this的最终指向。
箭头函数中的 this
箭头函数是 ES6 中引入的一种特殊函数,它没有自己的 this,而是继承自外层作用域的 this。也就是说,箭头函数的 this 并不是在调用阶段确定的,而是在定义阶段就已经确定,并且之后无法再被修改。
JAVASCRIPTconst obj = { name: "obj", sayName: function () { console.log(this.name); }, }; const arrowFn = () => { console.log(this.name); }; obj.sayName(); // obj arrowFn.call(obj); // window 或 globalThis
需要注意的是,箭头函数的 this 绑定是在定义时确定的,而不是在调用时。这意味着,箭头函数的 this 指向是固定的,不能通过 call / apply / bind 等方法修改。
为什么要这样设计?
箭头函数的设计目的,并不是为了替代普通函数,而是为了简化函数体和在某些场景下消除 this 带来的不确定性。当你希望 this 与外层上下文保持一致时,箭头函数往往是更合适的选择。
例如,在事件处理函数中,使用箭头函数可以避免 this 指向问题:
JAVASCRIPTconst btn = document.querySelector("button"); btn.addEventListener("click", () => { console.log(this); // window 或 globalThis });
这里的 this 指向是固定的,不会因为事件冒泡而改变。
this 指向不确定性
可能大家都会有疑惑为什么函数执行上下文有自己的 this,那为什么这个 this 的值还要“看是被谁调用的”?
这听起来就像是 this 已经是上下文的一部分了但又依赖外部调用关系。其实矛盾的根本在于:执行上下文一定会创建 this,但它并不自己决定“值是什么”。
但其实很好理解:
- 执行上下文:定义了「这里需要一个 this」
- 调用形式:决定了「这个 this 指向谁」
来看代码:
JAVASCRIPTfunction foo() { console.log(this); } foo(); // 和 const obj = { foo }; obj.foo();
注意关键点函数体完全一样,函数定义完全一样,但调用表达式不同,引擎在创建函数执行上下文时,会回头看一件事:“这个函数,是通过什么形式被调用的?”也就是说:
- 有没有调用者对象
- 是否通过
new - 是否使用了
call / apply / bind
那为什么会设计成这样呢?这可能就涉及到 JavaScript 的设计哲学了。 在 JS 中函数是一等公民,它可以被:
- 赋值
- 传参
- 复用
- 在多个对象之间共享
如果
this在定义时就确定,**那么这个函数永远只能绑定一个对象,失去可复用性。**所以可以理解为this的本质是服务于”方法服用“。
JSfunction sayName() { console.log(this.name); } const a = { name: "A", sayName }; const b = { name: "B", sayName }; a.sayName(); // A b.sayName(); // B
sayName 是同一个函数,但 this 在不同调用中指向不同对象。这正是 this 存在的意义。
如何打破 bind 绑定
上面我们说过,bind 方法返回的函数,其 this 值永久绑定到指定对象,后续再通过 call / apply 也无法修改其指向。
那如果我们想在某些场景下,临时改变 this 的指向,该怎么办?
答案是:使用 new 方法。
new不是“打破”了bind,而是new拥有更高优先级的 this 绑定规则。
先看现象:为什么 bind 会“失效”
JAVASCRIPTfunction Foo(name) { this.name = name; } const BoundFoo = Foo.bind({ name: "bind" }); const f = new BoundFoo("new"); console.log(f.name); // 'new'
你可能会认为是 bind 失效了,但结果是 bind 的 this 完全没用上。
这就要看真正发生了什么:
bind 到底做了什么?bind 并不是简单“改 this”,它做的是:
- 创建一个
Bound Function - 内部保存:
[[BoundThis]][[BoundArgs]][[TargetFunction]]也就是说
JAVASCRIPTBoundFoo = { [[TargetFunction]]: Foo, [[BoundThis]]: { name: "bind" }, };
new 到底做了什么? 当你执行:
JAVASCRIPTnew BoundFoo('new')
会设置新的原型,指向 BoundFoo.prototype。所以可以 new 可以打破 bind 绑定。
那为什么 bind 还“支持” new 调用?
bind 并不是“禁止 new”,而是 兼容构造函数场景 规范中明确规定:
- 如果 bound function 被
new调用, - 那么 [[BoundThis]] 会被忽略。 但[[BoundArgs]] 仍然有效,原型链仍然要正确。
JAVASCRIPTfunction Foo(a, b) { this.sum = a + b; } const BoundFoo = Foo.bind(null, 1); const f = new BoundFoo(2); console.log(f.sum); // 3
其中 this 是 new 创建的对象, 1 是 bind 时预置的参数。而这么做的核心目的只有一个:让 bind 后的函数,仍然可以作为构造函数使用。
否则下面这种代码将完全不可用:
JAVASCRIPTfunction Person(name) { this.name = name; } const User = Person.bind(null); const u = new User('arafat');
这在框架、工厂函数、装饰器中是非常重要的能力。
总结
在 JavaScript 中,this 并不是固定的对象,而是函数在执行上下文中根据调用方式动态绑定的值。理解 this 的核心在于区分函数类型和调用场景,而不是死记规则。
- 判断 this 时,先看调用方式,再看函数类型。
- 箭头函数用于稳定 this,普通函数用于对象方法和构造函数。
- bind 返回的新函数可以固定 this,但注意 new 会覆盖绑定。
- 实践中多写示例,推导 this 指向,理解比死记优先。
总而言之,理解 this 的关键不在于死记规则,而在于理解执行上下文 + 调用方式 + 函数类型之间的关系。掌握这一点,你就能在复杂回调、对象方法、构造函数和箭头函数中自信地判断 this 的指向。
如果你看到这里,谢谢你花时间阅读这篇小小的开篇文章。希望未来能与你在技术的旅途中有更多交流。