this 我是被输出题和 React 类组件坑出来的:绑定丢失、箭头函数不绑定、调用位置决定指向,这几点理清后就不容易错。
Q1: this 的四大绑定规则与优先级
1
this 不是在定义时确定的
this 的核心法则是:它不是在定义时确定的,而是在代码执行(调用)时动态绑定的(箭头函数除外)。2
默认绑定: 独立调用
当函数被直接调用(如
fn())时,this 指向全局对象 window。在严格模式下,this 为 undefined。3
隐式绑定: 对象调用
当函数作为某个对象的方法被调用(如
obj.fn())时,this 指向那个最后的调用者对象。4
显式绑定: call/apply/bind
通过
call、apply、bind 强制指定 this。bind 属于硬绑定,返回新函数,this 无法再被普通的 call/apply 修改。5
new 绑定: 构造调用 (优先级最高)
当使用
new 关键字调用函数时,引擎会创建一个全新的空对象,并将 this 指向这个新对象。new 能覆盖 bind。this 绑定规则图解
四大绑定规则可以用一个漏斗模型来记忆——从优先级最高的规则开始,逐层向下匹配。箭头函数是特例,直接跳出整个漏斗。this 指向的逐步判断流程
遇到任何this 相关的输出题,按照下面的流程图逐层判断,永不踩坑:
四种绑定的代码示例(带逐行注释)
this 的优先级是 new > 显式(bind) > 隐式 > 默认。箭头函数跳出整套规则,直接继承外层 this。
陷阱: 隐式绑定丢失
把对象的方法赋值给变量,然后独立调用:
破局: 箭头函数的优势
箭头函数根本没有自己的 this!它在被定义的那一刻起,就顺着词法作用域往上找,牢牢捕获外层的
this。Q2: 输出题解题框架——如何快速判断 this 指向
面试中this 输出题出现频率极高,掌握一套固定的分析框架可以做到秒杀。
解题四步法
找调用点,看形式,判规则,定 this。箭头函数不用看调用点,直接往上找。
经典输出题图解:对象方法调用
题目 1:obj.fn() vs const fn = obj.fn; fn()
方法赋值给变量后独立调用 = 隐式绑定丢失。这是 this 输出题的头号陷阱。
经典输出题图解:回调函数中的 this
题目 2:setTimeout(obj.fn, delay)
setTimeout(fn, delay) 传的是函数引用,执行时丢失调用者上下文。用箭头函数包裹或 .bind(this) 修复。经典输出题图解:Class 中的 this 丢失
题目 3:Class 方法引用丢失Class 方法定义在 prototype 上,解构或传参后会丢失 this。用 constructor bind 或箭头函数类属性修复。
经典输出题图解:箭头函数中的 this
题目 4:箭头函数继承外层 this箭头函数的 this 在定义时锁定为外层普通函数的 this。如果外层 this 丢了,箭头函数也救不了。
经典输出题图解:链式调用中的 this
题目 5:链式调用中 this 的变化链式调用中 this 不变,前提是每次调用都是 obj.method() 形式。一旦脱离对象独立调用,this 立即丢失。
Q3: 手写 new 操作符的四步走
new 的四步底层拆解
new 与 bind 的优先级冲突
当函数被bind 绑定后,再用 new 调用,this 会指向新实例而非 bind 的目标。这是规范明确规定的:[[Construct]] 内部方法会强制覆盖之前的 this 上下文。
构造函数返回值的特殊行为
如果构造函数返回一个非 null 的对象,new 表达式的结果就是该对象,而非新创建的实例。这个特性可以用来实现单例模式。
Q4: ES6 Class 的本质与继承陷阱
ES6 的class 被称为原型链的语法糖。但 V8 对 class 做了极其严格的底层加固:
class内部自带严格模式- 方法默认
enumerable: false(不可枚举) - 必须且只能通过
new调用,直接调用会报错
class 与 ES5 构造函数的本质区别
extends 与 super 做了什么?
继承的核心在于super 关键字的两种形态:
- 当作函数调用:
super(name)本质上是Parent.call(this, name) - 当作对象访问:
super.method()本质上是在父类的prototype上去查找方法,然后强行把当前子类的实例作为this传进去执行
new.target 与原生构造函数继承
new.target 是 ES6 引入的隐藏指针,它指向当前被 new 的构造函数。子类调用 super() 时,new.target 会指向子类,导致底层分配的内部槽(Internal Slots)是子类结构而非父类结构。这是 ES5 无法实现原生构造函数继承的根本原因。
底层源码:V8 中的 this 与 class
this 在 V8 底层究竟是怎么解析的?
this 的四种绑定规则并不是引擎里面写了四个 if-else 分支,而是依赖底层对象结构的设计(详见 V8 源码:src/objects/js-function.h):
- 普通函数:通过内部方法
[[Call]]执行,它的this参数是在运行时动态传入的。 - 箭头函数:在 V8 底层的
SharedFunctionInfo中,其[[ThisMode]]被死死标记为kLexical。当引擎准备为它解析this时,一旦发现是kLexical,会直接跳过运行时动态绑定阶段,强行去读取闭包上下文Context中的this。
ES6 Class 到底比 ES5 函数高级在哪里?
V8 专门给 Class 赋予了 FunctionSyntaxKind::kClassConstructor 的标记,这会直接改变编译器的内联缓存(Inline Cache)策略。更关键的是,Class 继承原生构造函数(如 Array)能正确工作,是因为 ES6 的 [[Construct]] 内部机制引入了 new.target 这一隐藏指针。子类在调用 super() 时,new.target 会指向子类,导致底层分配的内部槽(Internal Slots)是完美的子类结构,这是 ES5 的 Array.call(this) 做不到的。
十、及格回答 vs 满分回答
问题:“说说 JavaScript 中的 this 机制?”
及格回答(60-70分):“this 指向调用它的对象。四种绑定规则:默认绑定指向 window,隐式绑定指向调用对象,显式绑定通过 call/apply/bind 强制改 this,new 绑定指向新出来的实例。优先级是 new > bind > 隐式 > 默认。”问题出在:死记硬背型选手。 能背出规则表,但一旦放到复杂的 React 闭包或者箭头函数嵌套里必挂无疑。 满分回答(90-100分):
“this是执行上下文中的动态指针。其绑定规则遵循new > 显式(bind) > 隐式 > 默认的优先级。new能覆盖bind,是因为底层[[Construct]]初始化新对象后,会强制覆盖之前的this上下文。[1]”
“箭头函数完全不参与这套动态绑定大战。在 V8 编译期,它就被硬性打上了[[ThisMode]] = kLexical的标签。引擎甚至不为它分配动态绑定的资源,而是将其视为自由变量,直接沿词法环境去外层抓取,因此任何call/bind都对其无效。[2]”
“在 React 类组件回调传参等实际业务中,由于独立调用导致的’隐式绑定丢失’是报错重灾区。通过constructor中的提前bind或使用箭头函数属性是标准的工程化解法。ES6 Class 继承原生构造函数(如Array)能正确工作,是因为new.target隐藏指针保证了子类内部槽的正确分配。[3]“
常见追问
手写一个彻底的 bind, 并兼容 new 调用
手写一个彻底的 bind, 并兼容 new 调用
答题要点:
利用闭包返回新函数,检测 new.target。高分回答:
“核心逻辑分两步:第一步用
apply 绑定上下文。第二步最关键,要在返回的包装函数内部做一个拦截:if (this instanceof boundFn),如果为 true,说明它正在被 new 关键字作为构造器调用,此时必须无视我们之前传入的上下文 context,老老实实把原本构造函数的 this 指向当前这个新实例!”super.method() 里的 this 指向谁?
super.method() 里的 this 指向谁?
答题要点:
搞清方法出处与执行上下文的分离。高分回答:
“指向的是子类实例,绝不是父类实例。
super.method() 这个语法的底层逻辑是:去父类的 prototype 上翻出那个方法,但是以当前子类实例作为 this 来执行它。所以即使在调用父类方法,如果父类方法里有 this.name,它读出来的依然是子类实例上的 name 属性。”call, bind, new 的优先级到底怎么排?
call, bind, new 的优先级到底怎么排?
答题要点:
用代码验证优先级。高分回答:
“new 的优先级最高。即使函数已经被 优先级:new > bind > call/apply > 隐式 > 默认。”
bind 强行死绑到了某个对象上,一旦遭遇 new 关键字,bind 指定的 this 也会被直接废弃,this 被强行指向了新创建的实例。验证代码:箭头函数的 this 为什么不能被 call/bind 修改?
箭头函数的 this 为什么不能被 call/bind 修改?
答题要点:
从 V8 底层的 ThisMode 标记解释。高分回答:
“箭头函数在 V8 编译期就被硬性打上了
[[ThisMode]] = kLexical 的标签。引擎甚至不为它分配动态绑定的资源,而是将其视为自由变量,直接沿词法环境去外层抓取。因此 call、apply、bind 的第一个参数会被完全忽略——箭头函数根本不走 [[Call]] 的 thisArgument 流程。”对象方法中的 this 在不同调用方式下如何变化?
对象方法中的 this 在不同调用方式下如何变化?
答题要点:
对比方法调用与独立调用。高分回答:
“核心:把方法赋值给变量后独立调用,会导致隐式绑定丢失,退化为默认绑定。这是最常见的
this 的值完全取决于调用方式而非定义位置。同一个方法:this 相关 bug。”setTimeout 中的 this 指向谁?
setTimeout 中的 this 指向谁?
答题要点:
setTimeout 回调是独立调用,this 退化为默认绑定。高分回答:
“解决方案:使用箭头函数捕获外层
setTimeout 的回调函数是独立调用的,因此 this 指向 window(非严格模式)或 undefined(严格模式)。this,或在调用前 bind(this)。”class 中的 this 指向和普通函数有什么不同?
class 中的 this 指向和普通函数有什么不同?
答题要点:
class 内部自动严格模式 + 必须 new 调用。高分回答:
“ES6 class 内部自动开启严格模式,因此直接调用构造函数时
this 为 undefined(而非 window),引擎直接报错。此外,class 中定义的方法在底层依然存在于 prototype 上,方法内的 this 遵循相同的绑定规则——谁调用指向谁。如果把 class 方法传给 setTimeout,this 同样会丢失。”在 React 中如何彻底解决 this 丢失问题?
在 React 中如何彻底解决 this 丢失问题?
答题要点:
对比四种方案的优劣,推荐最佳实践。高分回答:
“四种方案:
- constructor 中 bind:最推荐,只创建一次函数实例,不影响每次 render 的性能。
- 箭头函数类属性:最简洁,需要 babel 插件支持,底层每次创建新函数引用。
- render 中 bind:
<button onClick={this.handleClick.bind(this)}>,每次 render 创建新函数,影响子组件 diff。 - render 中箭头函数:同上,每次 render 创建新函数。
this 的问题,这是 React 团队推荐的方向。“延伸阅读与引用参考
- MDN Web Docs: this:MDN 官方对 this 机制的详尽文档。 [1]
- V8 Blog: JavaScript ES6 class semantics:V8 团队解析 ES6 class 在引擎中的实现细节。 [2]
- V8 Source Code: js-function.h:V8 中函数对象的 C++ 头文件,包含箭头函数的 this 模式定义。 [3]
