本节摘要:this 不是函数自带的属性,也不是作用域链上的变量,而是在调用发生、栈帧创建的那一刻由「调用方式」决定的引用。判定按优先级走四条规则:new 绑定、显式绑定(call、apply、bind)、隐式绑定(谁在点号前面)、默认绑定(严格模式 undefined,非严格全局对象);箭头函数完全跳出这套规则,沿用定义时的外层 this。本节给出可机械执行的判定流程,并用实测代码拆穿常见误判。
一段代码感受 this 的「看调用方式下菜」:
function show() { console.log(this === globalThis ? 'global' : this.label ?? 'other'); } show(); // global(默认绑定) const box = { label: 'box', show }; box.show(); // box(隐式绑定) box.show.call({ label: 'called' }); // called(显式绑定) new (show)(); // other(new 绑定,this 指向新对象) const bound = show.bind({ label: 'bound' }); bound(); // bound(bind 优先于隐式) setTimeout(box.show, 0); // global(引用被抽出,隐式绑定丢失)
最后一行是高频翻车点:把方法当值传出去,点号没了,隐式绑定随之丢失,回调执行时只剩默认绑定。这就是「把 this.printState 直接塞进 setTimeout 就不对了」的引擎层解释。
把判定写成流程,从上往下第一条命中的就是答案:
规则一:new 调用? this 指向新创建的对象。构造调用会先造一个空对象(原型接到构造器的 prototype 上),把 this 指给它执行函数体,默认返回它。
规则二:call、apply、bind 显式指定? this 指向传入的对象。三者差别只在传参方式:call 逐个传,apply 传数组,bind 返回永久绑定的新函数(且绑定后无法再被 call 改写)。非严格模式下传 null 或 undefined 会被替换成全局对象。
规则三:有接收者(点号调用)? this 指点号前面的那个对象。只看最后一层点号:a.b.c.fn() 里 this 是 c,不是 a。
规则四:以上都不是(裸调用)。 严格模式 this 是 undefined,非严格模式被引擎改成 globalThis。这就是「测试里 this 指向 window/global」的来源。
实测优先级——显式绑定压过隐式,bind 压过 call:
const obj = { v: 1, get() { return this.v; } }; const fixed = obj.get.bind({ v: 99 }); console.log(obj.get()); // 1 console.log(obj.get.call({ v: 2 })); // 2 console.log(fixed()); // 99 console.log(fixed.call({ v: 3 })); // 99(bind 之后的 this 锁死)

箭头函数没有自己的 this 绑定机制。创建时,它把外层执行上下文的 this 抄一份存进函数对象,之后无论怎么调用都沿用这份副本:
const timer = { seconds: 0, start() { setInterval(() => { this.seconds++; // 这里的 this 来自 start 的栈帧 if (this.seconds === 3) console.log('3秒到'); }, 1000); } }; timer.start(); // 3秒到(隐式绑定 this=timer,箭头函数抄走)
若把箭头换成普通函数,this.seconds 里 this 是默认绑定(非严格即全局),NaN 累加三次后条件永假,静默失败。事件回调、数组迭代器、Promise 回调里优先用箭头函数,就是借用它「抄外层」的特性。
反过来的坑也存在:箭头函数当对象方法,this 抄到的是模块/全局层,拿不到对象自身:
const bad = { name: 'bad', greet: () => console.log(this.name) }; bad.greet(); // undefined(this 是外层,不是 bad)
对象字面量里的方法老老实实用普通函数或方法简写 greet() { ... }。
类字段初始化器与箭头函数的组合是 React 老式类组件的标准手法:this.handleClick = () => {...} 在构造时抄走组件实例的 this,天然免疫事件系统的调用方式。Hooks 时代函数组件没有 this 问题,这个手艺正在退场,但老代码里大量存在,读得懂仍是必需。
判例一:回调里的隐式丢失。
class Counter { count = 0; inc() { this.count++; } report() { return this.count; } } const c = new Counter(); const inc = c.inc; inc(); // TypeError: Cannot read properties of undefined
类体默认严格模式,抽出方法裸调用,this 是 undefined,读取 count 直接抛错。修法:绑死(构造器里 bind)、改箭头函数字段、或调用处 () => c.inc()。
判例二:数组方法的第二参救场。
const cart = { rate: 0.9, items: [100, 200] }; console.log(cart.items.map(function (v) { return v * this.rate; }, cart)); // [90, 180](map 的第二个参数给回调做 thisArg)
判例三:原型方法被借用。
function speak() { return this.lang; } const dog = { lang: 'woof' }; const cat = { lang: 'meow' }; console.log(speak.call(dog), speak.call(cat)); // woof meow
同一份函数体,两份 this——「函数是值、this 是调用时才发的工牌」这句总结在这里最直观。
判例四:嵌套函数的经典误判。
const app = { name: 'app', run() { function helper() { return this?.name; } console.log(helper()); // undefined(非严格:globalThis.name) console.log(this.name); // app } }; app.run();
this 不沿作用域链查找!内层裸调用的 this 走默认绑定,不会「继承」外层。这是从 Java、Python 转过来的开发者最顽固的直觉错误。修法照旧:内层改箭头函数,或 const self = this(老派但有效)。
⚠️ 常见坑:DOM 事件监听器里普通函数的 this 是触发事件的元素(addEventListener 隐式把元素当接收者),不是书写代码时的对象;用箭头函数时 this 则沿用绑定时的外层。两种写法 this 不同,混用前先想清楚要哪个。
六道判定题,先按流程卡自己判,再看答案。每题都可直接运行验证。
第一题:
const obj = { name: 'obj', getName() { return this.name; } }; const { getName } = obj; console.log(getName()); // undefined(解构抽出,裸调用默认绑定,this 是 globalThis)
第二题:
function F() { this.v = 1; return { v: 2 }; } console.log(new F().v); // 2(构造器返回了对象,new 的默认返回被覆盖) console.log(F().v); // 2(裸调用时 this 是 globalThis,返回值同样是那个对象)
第三题:
const arr = [1, 2, 3]; Array.prototype.last = function () { return this[this.length - 1]; }; console.log(arr.last()); // 3(隐式绑定:this 是 arr)
第四题:
class Timer { constructor() { this.s = 0; } start() { setInterval(function () { this.s++; }, 1000); // 普通函数:默认绑定 } } const t = new Timer(); t.start(); setTimeout(() => console.log(t.s), 2500); // 0(setInterval 回调改的是全局的 s,不是 t 的)
第五题:
const logger = { prefix: '日志', log(...args) { console.log(this.prefix, ...args); } }; const bound = logger.log.bind(logger); setTimeout(bound, 0, '消息'); // 日志 消息(bind 锁定,抽走传递也不丢)
第六题:
const handler = { host: 'handler', run: () => console.log(this === globalThis ? 'global' : this.host) }; handler.run(); // global(箭头函数抄的是模块/全局层 this,点号救不了)
六题的分布覆盖了全部规则:解构丢失(一)、构造器返回覆盖(二)、原型方法(三)、回调默认绑定(四)、bind 锁定(五)、箭头抄外层(六)。错哪题就回看对应小节——判定流程是机械的,熟练度来自把每一步说出口。
箭头函数能当构造器用吗?
不能,new 箭头函数 直接抛 TypeError:没有 [[Construct]] 内部方法,也没有自己的 this 与 arguments。同理它也没有 prototype 属性。这四个「没有」是同一件事的四面:箭头函数就是「不带上下文的函数体」。
事件监听里的 this 到底是谁?
addEventListener 用普通函数时,this 是「挂监听的元素」(隐式绑定,接收者是元素);用箭头函数时,沿用「绑定那一刻」的外层 this。要操作触发元素时用 event.target 更直接——currentTarget 在委托场景里尤其有用(第 5 章会展开)。
有了箭头函数,bind 还有用吗?
有,三类场景:把方法转成「永久绑定」的引用存进映射表(分发器模式);偏函数应用(先固定前几个参数);给高阶函数传带 thisArg 语义的回调。日常开发里箭头函数覆盖了八成需求,剩下两成正是 bind 的精准领地。
把四条规则落到日常代码的典型位置上,做成一张速查表。每行都可以在控制台秒验:
| 代码位置 | this 是谁 | 依据的规则 |
|---|---|---|
对象方法 obj.fn() |
obj(最后一层点号) | 隐式绑定 |
类方法 inst.fn() |
inst 实例 | new 绑定的延续 |
| 事件监听普通函数 | 挂监听的元素 | addEventListener 隐式传入 |
| 事件监听箭头函数 | 绑定时刻的外层 this | 箭头抄外层 |
| setTimeout 普通回调 | globalThis(严格模式 undefined) | 默认绑定 |
| setTimeout 箭头回调 | 定义处外层 this | 箭头抄外层 |
| 模块顶层裸 this | undefined(模块默认严格模式) | 默认绑定 |
| 箭头函数对象方法 | 模块/全局层 this,拿不到对象 | 箭头抄外层 |
| call 与 apply 调用 | 指定的对象 | 显式绑定 |
| 构造调用 new | 新创建的对象 | new 绑定 |
三行箭头函数条目值得连读:箭头函数的位置决定它抄谁——写在对象方法里的箭头抄到方法级的 this(实例),写在模块顶层抄到 undefined。所以「箭头函数拿不到 this」是误传,它只是不参与绑定游戏、永远沿用出生地外层的那份副本。
配套一个综合小实验,把速查表的边界感跑出来:
const widget = { name: '组件', items: ['a', 'b'], show() { this.items.forEach(function (i) { console.log(this === globalThis, i); // true —— 回调裸调用,默认绑定 }); this.items.forEach((i) => { console.log(this.name, i); // 组件 a / 组件 b —— 箭头抄了 show 的 this }); } }; widget.show();
同一个 forEach,两种回调两种 this——速查表的第 5 行与第 6 行在同一处代码里同台对比。日常写回调前扫一眼这张表,「这里 this 是谁」基本可以脱稿作答。
「隐式绑定丢失」是 this 事故的最大来源(方法被抽出传递、回调转发、解构赋值),四种修复模式各有适用面,一张表加四行代码收齐:
| 修复模式 | 写法 | 适用 | 代价 |
|---|---|---|---|
| 箭头包裹 | () => obj.fn() |
传回调给高阶函数 | 每次创建新函数(列表 key 场景注意) |
| 箭头字段 | fn = () => {...} 定义在类字段 |
类的方法要进回调 | 每实例一份函数,内存略增 |
| 构造器 bind | 构造器里 this.fn = this.fn.bind(this) |
老式类组件 | 样板代码多 |
| API 自带 thisArg | arr.forEach(fn, obj) |
支持它的内置方法 | 只覆盖内置高阶函数 |
四种写法的效果一致(this 锁定为目标),选择看语境:临时传参用箭头包裹最轻;类组件与需要稳定身份的场景用箭头字段或 bind;内置方法有机会就用 thisArg,零额外分配。反面清单同样重要:不要在对象字面量里用箭头函数当方法(抄不到对象自己),不要用 call 修正「以为丢了」的 this 而不先确认它真的丢了——多数「this 又不对了」其实是作用域看错了对象,先打印 this 再动手。
const cart2 = { items: [{ price: 10 }, { price: 20 }], rate: 0.9, total() { return this.items.reduce((sum, it) => sum + it.price, 0) * this.rate; // reduce 的回调是箭头函数,this 沿用 total 的 this(对象)—— 54 } }; console.log(cart2.total()); // 54
这个「外层普通方法定 this、内层箭头沿用」的组合,是日常代码里最常用也最不易错的形态,可以作为团队的默认写法。
面试里 this 题的解题步骤能标准化吗?
能,四步走,每题通用。第一步看函数形态:箭头函数直接抄外层,答题结束;普通函数继续。第二步找调用点:new 优先,其次 call、apply、bind,其次点号前的接收者,最后默认绑定。第三步查严格模式:默认绑定的答案取决于此(undefined 还是全局对象),类体与模块必严。第四步验证边界:方法被抽出传递了没有、事件监听谁挂的、setTimeout 里写的哪种函数。四步走完还不确定的 this 题,多半题目本身在玩「调用点隐藏」(比如赋值给变量再调用、作为参数传递),把调用点的形态还原出来,答案自然浮出。把这套步骤练成条件反射,this 题从「最怕的题型」变成「送分题」。
严格模式的 this 还有哪些连带变化?
除了默认绑定给 undefined 之外,严格模式整体是「少替你兜底」的取向:静默失败改为抛错(给只读属性赋值、删不可配置属性)、arguments 不再与形参联动、八进制字面量禁用。对 this 主题最重要的一条正是默认绑定的变化——它把「this 意外指向全局然后在全局上创建了一堆变量」这类事故,变成当场抛错的明故障。模块与类默认严格,所以新代码天然享有这层保护;写普通脚本时主动声明严格,是低成本高回报的习惯。
回头看本节开头的六种调用:现在每一种你都能沿判定流程走到底并说清「为什么」。配套的还有速查表(十个典型位置的 this 归属)、修复模式对照(绑定丢失的四种解法各自的使用面)与四步解题流程(面试 this 题的标准化打法)。this 的全部秘密不过是「调用方式决定栈帧里的那个引用」——下一节我们看栈帧弹出之后,留在堆里的那些东西如何让变量活得比函数更久。