Skip to main content
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 就像一个麦克风,谁拿着麦克风(谁调用函数),声音就从谁那里出来。obj.fn() 是 obj 拿着麦克风,this 就是 obj。把方法赋值给变量 const fn = obj.greet; fn() 相当于把麦克风抢过来自己用,this 就变成了全局对象。bind 就是用胶带把麦克风粘死在某人手上,怎么抢都抢不走。而箭头函数更狠——它根本没有麦克风,它直接借外层人的麦克风用,绑定的胶带都撕不掉。
this 的优先级漏斗:

this 绑定规则图解

四大绑定规则可以用一个漏斗模型来记忆——从优先级最高的规则开始,逐层向下匹配。箭头函数是特例,直接跳出整个漏斗。

this 指向的逐步判断流程

遇到任何 this 相关的输出题,按照下面的流程图逐层判断,永不踩坑:

四种绑定的代码示例(带逐行注释)

优先级验证流程图:
this 的优先级是 new > 显式(bind) > 隐式 > 默认。箭头函数跳出整套规则,直接继承外层 this。

陷阱: 隐式绑定丢失

把对象的方法赋值给变量,然后独立调用:

破局: 箭头函数的优势

箭头函数根本没有自己的 this!它在被定义的那一刻起,就顺着词法作用域往上找,牢牢捕获外层的 this。

Q2: 输出题解题框架——如何快速判断 this 指向

面试中 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 中 this 丢失的标准修复:
Class 方法定义在 prototype 上,解构或传参后会丢失 this。用 constructor bind 或箭头函数类属性修复。

经典输出题图解:箭头函数中的 this

题目 4:箭头函数继承外层 this
箭头函数的 this 在定义时锁定为外层普通函数的 this。如果外层 this 丢了,箭头函数也救不了。

经典输出题图解:链式调用中的 this

题目 5:链式调用中 this 的变化
链式调用 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 关键字的两种形态:
  1. 当作函数调用:super(name) 本质上是 Parent.call(this, name)
  2. 当作对象访问:super.method() 本质上是在父类的 prototype 上去查找方法,然后强行把当前子类的实例作为 this 传进去执行
在 ES5 中,继承是”先创造子类实例的 this,再把父类的属性强加给它”。而在 ES6 Class 中,继承机制完全反转:子类根本没有自己的 this!它必须先调用 super(),由父类完成底层的 this 构建和实例化,然后子类才能拿到这个 this 进行加工。如果你在 super() 之前访问 this,引擎会直接抛出 ReferenceError。

new.target 与原生构造函数继承

new.target 是 ES6 引入的隐藏指针,它指向当前被 new 的构造函数。子类调用 super() 时,new.target 会指向子类,导致底层分配的内部槽(Internal Slots)是子类结构而非父类结构。这是 ES5 无法实现原生构造函数继承的根本原因。

底层源码:V8 中的 this 与 class

this 的四种绑定规则并不是引擎里面写了四个 if-else 分支,而是依赖底层对象结构的设计(详见 V8 源码:src/objects/js-function.h):
  • 普通函数:通过内部方法 [[Call]] 执行,它的 this 参数是在运行时动态传入的。
  • 箭头函数:在 V8 底层的 SharedFunctionInfo 中,其 [[ThisMode]] 被死死标记为 kLexical。当引擎准备为它解析 this 时,一旦发现是 kLexical,会直接跳过运行时动态绑定阶段,强行去读取闭包上下文 Context 中的 this。
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]“

常见追问

答题要点: 利用闭包返回新函数,检测 new.target。高分回答: “核心逻辑分两步:第一步用 apply 绑定上下文。第二步最关键,要在返回的包装函数内部做一个拦截:if (this instanceof boundFn),如果为 true,说明它正在被 new 关键字作为构造器调用,此时必须无视我们之前传入的上下文 context,老老实实把原本构造函数的 this 指向当前这个新实例!”
答题要点: 搞清方法出处与执行上下文的分离。高分回答: “指向的是子类实例,绝不是父类实例。super.method() 这个语法的底层逻辑是:去父类的 prototype 上翻出那个方法,但是以当前子类实例作为 this 来执行它。所以即使在调用父类方法,如果父类方法里有 this.name,它读出来的依然是子类实例上的 name 属性。”
答题要点: 用代码验证优先级。高分回答: “new 的优先级最高。即使函数已经被 bind 强行死绑到了某个对象上,一旦遭遇 new 关键字,bind 指定的 this 也会被直接废弃,this 被强行指向了新创建的实例。验证代码:
优先级:new > bind > call/apply > 隐式 > 默认。”
答题要点: 从 V8 底层的 ThisMode 标记解释。高分回答: “箭头函数在 V8 编译期就被硬性打上了 [[ThisMode]] = kLexical 的标签。引擎甚至不为它分配动态绑定的资源,而是将其视为自由变量,直接沿词法环境去外层抓取。因此 call、apply、bind 的第一个参数会被完全忽略——箭头函数根本不走 [[Call]] 的 thisArgument 流程。”
答题要点: 对比方法调用与独立调用。高分回答: “this 的值完全取决于调用方式而非定义位置。同一个方法:
核心:把方法赋值给变量后独立调用,会导致隐式绑定丢失,退化为默认绑定。这是最常见的 this 相关 bug。”
答题要点: setTimeout 回调是独立调用,this 退化为默认绑定。高分回答: “setTimeout 的回调函数是独立调用的,因此 this 指向 window(非严格模式)或 undefined(严格模式)。
解决方案:使用箭头函数捕获外层 this,或在调用前 bind(this)。”
答题要点: class 内部自动严格模式 + 必须 new 调用。高分回答: “ES6 class 内部自动开启严格模式,因此直接调用构造函数时 this 为 undefined(而非 window),引擎直接报错。此外,class 中定义的方法在底层依然存在于 prototype 上,方法内的 this 遵循相同的绑定规则——谁调用指向谁。如果把 class 方法传给 setTimeout,this 同样会丢失。”
答题要点: 对比四种方案的优劣,推荐最佳实践。高分回答: “四种方案:
  1. constructor 中 bind:最推荐,只创建一次函数实例,不影响每次 render 的性能。
  2. 箭头函数类属性:最简洁,需要 babel 插件支持,底层每次创建新函数引用。
  3. render 中 bind:<button onClick={this.handleClick.bind(this)}>,每次 render 创建新函数,影响子组件 diff。
  4. render 中箭头函数:同上,每次 render 创建新函数。
最佳实践:函数组件 + Hooks 彻底消除了 this 的问题,这是 React 团队推荐的方向。“

延伸阅读与引用参考