2.2 this绑定的现场判定


2.2 this绑定的现场判定

本节摘要: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 锁死)

图 2.2-1 this 判定流程卡

图 2.2-1 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 不同,混用前先想清楚要哪个。

  • this 是调用时发的工牌:栈帧创建那一刻定案,跟函数定义在哪无关;
  • 四规则有序:new → 显式 → 隐式 → 默认,bind 的绑定不可再改;
  • 箭头函数抄外层:回调首选,但对象方法与需要动态 this 的场合禁用;
  • this 不查作用域链,嵌套函数不会继承外层 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

把四条规则落到日常代码的典型位置上,做成一张速查表。每行都可以在控制台秒验:

代码位置 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 的全部秘密不过是「调用方式决定栈帧里的那个引用」——下一节我们看栈帧弹出之后,留在堆里的那些东西如何让变量活得比函数更久。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U