this在函数调用时由V8引擎直接写入执行上下文栈帧的ThisBinding字段,既非变量也非属性。四大绑定规则(new、显式、隐式、默认)决定该字段的初始化值,箭头函数没有自己的ThisBinding字段,因此其this继承外层作用域。在setTimeout中传入obj.fn时,由于函数引用脱离原对象,调用时触发默认绑定导致this丢失。

要真正理解JavaScript中this绑定的底层原理,单纯记住“谁调用指向谁”是不够的。需要从V8引擎执行上下文栈的物理构建过程入手——this既不是变量也不是属性,而是每次函数调用时,V8引擎在压入新栈帧(stack frame)的瞬间,**直接写入当前执行上下文对象ThisBinding字段的一个确定值**。
V8执行上下文栈:this的物理容器与存储机制
V8引擎每次调用函数时,会在调用栈顶部创建全新的执行上下文对象。该对象在内存中是一个结构化实体,包含三个核心字段:LexicalEnvironment(词法环境)、VariableEnvironment(变量环境)和ThisBinding。其中ThisBinding是独立字段,不参与作用域链查找,也不受var/let声明影响——它完全由调用方式决定,并在上下文创建阶段完成赋值。
这意味着this的值并非运行时动态查找得到,而是在调用发生时被直接“塞入”栈帧。一旦栈帧形成,ThisBinding就已固定,后续所有对this的访问,本质上是读取该栈帧内存块中的对应字段。
你可以将函数调用想象成创建了一个小型运行环境容器,容器内有一个专用夹层存放this。区别在于这个夹层的内容并非通过变量声明存入,而是取决于你如何“推开这扇门”——也就是调用方式。
四大绑定规则对应栈帧初始化动作
V8在创建函数执行上下文之前,会解析整个调用表达式,按照优先级执行一套硬编码逻辑,最终将计算结果直接写入ThisBinding字段:
- new绑定:当遇到
new Foo()时,V8创建新对象,将其内存地址直接填入新上下文的ThisBinding字段。相当于V8说:“新对象已创建,this抽屉里放着它的钥匙。” - 显式绑定:遇到
fn.call(obj)或fn.apply(obj)时,V8将第一个参数(或undefined)原样写入ThisBinding。如同拿着地址纸条直接塞进抽屉。 - 隐式绑定:识别点号左侧最近的对象(如
obj.method()中的obj),将其引用写入ThisBinding。中间链(如a.b.c.method())不被考虑,引擎只关注最后一个点前面的对象。 - 默认绑定:当以上规则都不匹配时,严格模式下写入
undefined,非严格模式写入globalThis(浏览器中为window)。这是最后的兜底机制。
箭头函数没有ThisBinding字段
箭头函数在V8编译阶段即被标记为“词法this”,它不会创建自己的执行上下文ThisBinding字段。运行时访问this时,引擎沿词法环境链向上查找,复用外层普通函数执行上下文中的ThisBinding值。
这正是箭头函数this稳定不变的根本原因——并非被保护,而是它根本没有ThisBinding字段可供修改。因此call、apply、bind或new对它皆无效,因为箭头函数在编译时已决定不采用这些绑定规则。
为什么setTimeout(obj.fn, 100)会丢失this?
这是一个经典问题,几乎所有开发者都遇到过。关键在于obj.fn被取值后变成纯函数引用,传入setTimeout时已脱离原对象obj。当定时器触发回调时,V8执行的是一个孤立函数调用,调用表达式中没有点号、没有new、也没有显式绑定参数——只能走默认绑定。
此时新创建的执行上下文,其ThisBinding字段被设为undefined(严格模式)或window(非严格模式),与原始obj再无关联。这并非“丢失”,而是上下文重建时,物理上就没有写入那个对象。调用方式的改变,使得V8在物理层面上无法将obj的地址写入新栈帧的ThisBinding字段。
