题目 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)。