题目 31 / 31

当四条 this 规则同时出现,到底谁说了算?

知识点: this 绑定难度: 高级

默认绑定、隐式绑定,以及全部三种显式绑定形式——call、apply、bind——再加上 new 和一个箭头函数,全部串在一条链路上,让每条规则"覆盖上一条"的过程在同一个 trace 里清清楚楚地展示出来。

使用执行可视化器解题在下方逐步观察每个绑定、函数调用和控制台输出。

预测输出结果

同一个函数形状被连续调用十次,一次比一次被更多地覆盖。请预测全部十行输出,并能说清楚每一步到底是哪条绑定规则在起作用。

var a = "Window";

function identify() {
  console.log(this.a);
}

var obj1 = { a: "obj1", identify: identify };
var obj2 = { a: "obj2" };
var obj3 = { a: "obj3" };

identify();
obj1.identify();
obj1.identify.call(obj2);
obj1.identify.apply(obj3);

var boundIdentify = obj1.identify.bind(obj2);
boundIdentify();
boundIdentify.call(obj3);
boundIdentify.apply(obj3);

var arrowIdentify = () => console.log(this.a);
arrowIdentify.call(obj2);

function Identify(label) {
  this.a = label;
  console.log(this.a);
}

var BoundIdentify = Identify.bind(obj2);
var instance = new BoundIdentify("new-wins");
console.log(obj2.a);
查看答案与完整解析

正确输出

Window
obj1
obj2
obj3
obj2
obj2
obj2
Window
new-wins
obj2

为什么会这样

identify() 是一次裸调用——前面没有任何对象——因此走默认绑定,this 是全局对象(window),输出 "Window"。

obj1.identify() 是一次真正的方法调用:从调用点读到的接收者是 obj1,因此走隐式绑定,this.a 读到 "obj1"。

obj1.identify.call(obj2) 和 obj1.identify.apply(obj3) 都显式强制指定了接收者——call 和 apply 的区别只在于传参方式,绑定 this 的方式完全一样——因此显式绑定彻底覆盖了隐式绑定:this 依次是 obj2、obj3,输出 "obj2" 和 "obj3"。

obj1.identify.bind(obj2) 创建了一个永久绑定的函数。调用 boundIdentify() 输出 "obj2"——在它运行的这一刻,bind 也只是另一种显式绑定。

boundIdentify.call(obj3) 和 boundIdentify.apply(obj3) 都试图给一个已经绑定好的函数强加新的接收者——两次都失败了,仍然输出 "obj2"。根据 Function.prototype.call/apply 的规范,调用一个已绑定的函数时会直接忽略你传入的 thisArg:[[BoundThis]] 在 .bind() 运行的那一刻就已经固定下来,除了 new 之外没有任何东西能撼动它。

arrowIdentify 是一个箭头函数,因此它没有属于自己的 this——.call(obj2) 在语法上是被接受的,但完全不起作用。它捕获的是自己被创建时所在作用域的 this(也就是顶层的 window),不管之后怎么调用,都输出 "Window"。

BoundIdentify 是把 Identify 硬绑定到 obj2 上得到的函数。但 new BoundIdentify("new-wins") 是把它当作构造函数调用——根据 BoundFunctionExoticObject.[[Construct]] 的规范,构造一个已绑定的函数会完全忽略 [[BoundThis]],转而构造原始的 Identify,并以一个全新创建的实例作为 this。正是这个实例接收到了 "new-wins",这一行是从构造函数内部打印出来的。

console.log(obj2.a) 仍然读到 "obj2"——这证明上面那次 new 调用完全没有碰到 bind() 当初捕获的那个对象。这正是 new 能够压过一次硬绑定(hard bind)的原因:构造函数调用永远会拿到一个全新创建的对象作为 this,而不是之前绑定好的接收者。

这道题考察什么

  • 完整的优先级从低到高依次是:默认绑定 < 隐式绑定 < 显式绑定(call/apply/bind) < new 绑定——而箭头函数则完全不参与这套系统,它用的是外层作用域的 this
  • call 和 apply 是同一条显式绑定规则的两种不同传参方式;两者都无法覆盖 bind() 已经锁定好的接收者
  • 一旦 bind() 执行完毕,返回的函数在其整个生命周期内 this 都是固定的——之后无论多少次 call()/apply()(甚至再调用一次 bind())都无法改变它
  • new 是唯一能够压过硬绑定 this 的机制,因为按照规范,构造一个已绑定的函数会被重新路由到原始的目标函数上,并使用一个全新的实例,完全绕开了 [[BoundThis]]

运行环境前提

假设是经典的非严格模式脚本,此时一次没有接收者的调用其 this 默认是全局对象(window)。

规范参考

JavaScript 代码编辑器支持直接编辑或粘贴自定义代码
第 0 步 / 共 0 步
单步Space播放
按 → 单步 或 Space 自动播放
阶段 1: 编译预解析(变量提升与内存分配)
阶段 2: 代码执行(逐行赋值与调用求值)
单步执行原理解析

按键盘右方向键 “→” 进行单步调试,或按空格键 “Space” / 点击 “▶ 自动播放” 进行连续执行。