这是一个让我对 JavaScript 的 this 彻底改观的 bug,也是几乎每个 JS 开发者都会踩一次的"成人礼"。事情是这样的:我写了一个类,里面有个方法 handleClick,方法里要用到这个类实例的一些数据(this.state 之类)。开发时,我直接调用 instance.handleClick(),一切正常。可当我把这个方法作为"回调函数"传出去——比如绑给一个按钮的点击事件、或者塞进一个 setTimeout 里——等它真正被触发执行时,就当场报错了:Cannot read properties of undefined (reading 'state')。明明是同一个方法、同一段代码,我自己调它好好的,怎么一交给别人(事件系统、定时器)去调,this 就"丢了"、变成 undefined 了?
排查到最后,我才真正理解了 JavaScript 里 this 这个磨人的小妖精的本质——JS 里的 this,它的指向不是在函数"定义"时就固定下来的,而是在函数"被调用"的那一刻,根据"它是怎么被调用的"才动态决定的。当我用 instance.handleClick() 这样"通过实例点出来调用"时,this 指向 instance,一切正常;可当我把 handleClick 这个方法,从实例上"摘下来"、作为一个孤零零的回调函数传出去,等事件系统或定时器以 callback() 这种"光秃秃的方式"去调用它时,this 就和那个实例失去了联系——它要么指向 undefined(严格模式下),要么指向全局对象,反正不再是我期望的那个实例了。这篇文章,就从这次"方法一传出去 this 就丢了"的事故讲起,把 JavaScript 里 this 这个最反直觉、也最高频的坑,讲清楚。
故障现场:被"摘下来"就失忆的方法
先把这个"失忆"的过程还原一下:
class Counter {
constructor() { this.count = 0; }
increment() {
this.count++; // 依赖 this 指向 Counter 实例
console.log(this.count);
}
}
const counter = new Counter();
// 情况1: 通过实例"点"出来调用 → this 指向 counter, 正常
counter.increment(); // 1, 正常
// 情况2: 把方法"摘下来"作为回调传出去 → this 丢了!
setTimeout(counter.increment, 100); // 报错! 或 this 指向错误
// ↑ 等价于把 increment 函数拿出来、光秃秃地调用 callback(),
// 此时 this 不再是 counter, increment 里的 this.count 就炸了
// 情况3: 绑给事件监听, 同样的问题
button.addEventListener("click", counter.increment); // 点击时 this 丢了
// (在事件回调里, this 会指向触发事件的 DOM 元素 button, 而非 counter)
看出 this "失忆"的关键了吗?问题不在 increment 方法本身——它的代码一个字都没变;问题在于它"被调用的方式"变了。当你写 counter.increment() 时,JS 看到的是"通过 counter 这个对象去调用 increment",于是把 this 设成了 counter;可当你写 setTimeout(counter.increment, 100) 时,你其实是把 increment 这个函数本身(而不是"counter.increment 这个调用")当成值传了出去——函数和它原来所属的对象 counter 之间的"归属关系",在传递的那一刻就断了。等 100 毫秒后定时器去执行它时,它执行的是一个"无主"的函数(相当于 callback()),this 自然就不指向 counter 了。
这就是 this 最让人迷惑的地方:在很多其它语言里,一个对象的方法,this(或 self)是和对象牢牢绑定的、无论怎么调用都不会变;可在 JavaScript 里,this 不属于函数本身,它是在"调用现场"根据调用方式临时决定的。所以,把一个依赖 this 的方法"摘下来"单独传递、再被以"无主函数"的方式调用,this 就会和原对象失联——这,正是这个坑的全部根源。理解了"this 由调用方式动态决定",后面一切就都通了。
第一件事:理解 this 由"调用方式"决定,而非"定义位置"
要驯服 this,核心是建立一个和直觉相反、却至关重要的认知:JavaScript 里 this 的指向,取决于函数"如何被调用"(call-site),而不是函数"在哪里被定义"。同一个函数,以不同的方式调用,this 指向就不同。记住几种主要的调用方式和对应的 this:
function show() { console.log(this); }
const obj = { name: "obj", show };
// 1. 作为对象的方法调用(obj.方法()): this 指向那个对象 obj
obj.show(); // this → obj
// 2. 作为普通函数独立调用(直接 函数()): this 指向 undefined(严格模式)或全局
show(); // this → undefined / window ← 回调失联就是这种!
const fn = obj.show; // 把方法摘下来
fn(); // this → undefined / window ← 一摘下来单独调, this 就丢了
// 3. 用 call/apply/bind 显式指定: this 指向你指定的那个
show.call(obj); // this → obj (call 强行把 this 设成 obj)
// 4. 作为构造函数(new): this 指向新创建的实例
// 5. 箭头函数: 没有自己的 this, 用的是"定义时外层的 this"(下文重点)
关键认知是:this 是"动态绑定"的——它不是函数的固有属性,而是每次调用时,由"这个函数是怎么被调用的"来即时决定的。"通过对象点出来调用"(obj.fn()),this 就是那个对象;"光秃秃地独立调用"(fn()),this 就是 undefined/全局;"用 call/apply/bind 指定",this 就是你指定的。而回调失联的本质,正是:你把方法当值传出去时,传的只是那个"函数",丢掉了"通过哪个对象调用"这个上下文;等回调被触发时,它是被"独立调用"的(callback()),于是落到了第 2 种情况——this 丢了。所以,只要一个函数里用了 this、而它又可能被"摘下来当回调传递、再被独立调用",你就必须想办法,在传递时把它的 this "焊死"在正确的对象上——这就是后面要讲的几种解法。把"this 看调用方式、不看定义位置"这一条刻进脑子,你就抓住了理解 this 的钥匙。
第二件事:正解之一——箭头函数,把 this "锁死"在定义时的外层
解法有好几种,最常用、也最现代的是箭头函数。箭头函数有一个和普通函数本质不同的特性:它没有自己的 this;它内部的 this,永远是"它定义时所在的外层作用域的 this",而且这个绑定是固定的、不会随调用方式改变。正是这个特性,让它成了解决"回调里 this 丢失"的利器。
// 解法1a: 把回调写成箭头函数, 在里面调用方法 —— 箭头函数锁定外层 this
setTimeout(() => counter.increment(), 100);
// ↑ 箭头函数的 this 是定义时外层的, 这里它内部是 counter.increment()
// (我们仍是通过 counter.xxx 点出来调的, this 自然指向 counter)
// 解法1b(推荐): 用"类字段 + 箭头函数"定义方法, 一劳永逸地绑定 this
class Counter {
count = 0;
// 用箭头函数定义方法: increment 的 this 永远锁定为这个实例
increment = () => {
this.count++; // this 永远是创建它的那个实例, 怎么传都不丢
console.log(this.count);
};
}
const counter = new Counter();
setTimeout(counter.increment, 100); // 现在: 直接传也没问题! this 已被锁死
button.addEventListener("click", counter.increment); // 也没问题了
这两种箭头函数的用法,解决问题的角度略有不同:解法 1a是"包一层"——传给回调的是一个箭头函数 () => counter.increment(),这个箭头函数被调用时,它内部是通过 counter. 正常点出来调 increment 的,所以 this 没丢;解法 1b(更推荐)是"从源头锁死"——直接用"类字段 + 箭头函数"的语法来定义方法,这样 increment 这个箭头函数的 this,在它被创建(即实例被 new 出来)时,就永久地锁定为了那个实例,之后无论你怎么把它摘下来、传到哪里、以什么方式调用,它的 this 都岿然不动地指向原实例。解法 1b 尤其适合"一个方法注定会被频繁当作回调传递"的场景(比如 React 类组件的事件处理方法)——一次定义,永久绑定,再也不用操心 this 丢失。箭头函数"this 跟着定义走、不跟着调用变"的特性,正好对治了普通函数"this 跟着调用变"带来的麻烦,堪称 this 问题的现代标准解。我把"普通函数 vs 箭头函数"的 this 行为画成图:
这张图的核心对比:普通函数的 this 是"动态的"(跟着调用方式跑,容易在传递中丢失);箭头函数的 this 是"静态的"(定义时就锁定外层的 this,无论怎么调用都不变)。正因如此,凡是要"当回调传出去、又依赖 this"的地方,用箭头函数(或下面的 bind)把 this 锁死,就成了避坑的关键。
第三件事:正解之二——bind 显式绑定
另一种经典解法是用 bind。Function.prototype.bind(obj) 会返回一个"新函数",这个新函数的 this 被永久地绑定成了你指定的 obj,无论之后怎么调用它,this 都是 obj。
// 解法2: 用 bind 把方法的 this 绑定到实例, 得到一个 this 固定的新函数
setTimeout(counter.increment.bind(counter), 100); // bind 后, this 锁定为 counter
button.addEventListener("click", counter.increment.bind(counter));
// 常见做法: 在构造函数里一次性 bind 好, 之后直接用绑定版本
class Counter {
constructor() {
this.count = 0;
this.increment = this.increment.bind(this); // 把 increment 绑定到当前实例
} // 之后 this.increment 永远 this 正确
increment() { this.count++; console.log(this.count); }
}
// 注意区分 bind 和 call/apply:
// - call(obj, ...args): 立即调用, this=obj, 参数逐个传
// - apply(obj, [args]): 立即调用, this=obj, 参数用数组传
// - bind(obj): 不调用, 返回一个 this 绑定好的"新函数"(留着以后调)
bind 的作用,是"预先把 this 焊死,生成一个 this 固定的新函数",留着以后当回调用。它和箭头函数殊途同归(都是把 this 固定下来),只是写法不同:箭头函数是在"定义方法"时用语法锁定,bind 是在"传递方法"前用方法调用来锁定。常见的一个实践,是在类的构造函数里把需要当回调的方法都 bind(this) 一遍(这正是 React 类组件早年最经典的写法)。这里还要顺带厘清一组容易混淆的"近亲":call、apply、bind。它们都能显式指定 this,区别是:call 和 apply 是"立即用指定的 this 调用这个函数"(区别只在参数传法,call 逐个传、apply 用数组传);而 bind 是"不立即调用,而是返回一个 this 已绑定好的新函数"。当你需要"现在就以某个 this 调一下"用 call/apply,当你需要"生成一个 this 固定的函数留着以后(当回调)用"就用 bind。搞清这三者的区别,你就掌握了"手动控制 this"的全套工具。
第四件事:还有一个高发场景——回调里嵌套的普通函数
除了"方法被摘出去当回调",this 还有一个超高发的丢失场景,藏得更深:在一个方法内部,又写了一个普通函数(比如传给数组方法、定时器的回调),那个内层普通函数里的 this,也会丢!因为内层那个普通函数,被调用时也是"独立调用"的,它的 this 不会自动继承外层方法的 this。
class Cart {
constructor() { this.discount = 0.9; }
applyDiscount(items) {
// 外层方法 this 指向 Cart 实例, 正常
return items.map(function (item) {
return item.price * this.discount; // ← this 丢了! 这个内层普通函数的 this 不是 Cart!
}); // (map 回调被独立调用, this 是 undefined)
}
}
// 正解: 内层用箭头函数, 它继承外层 applyDiscount 的 this
class Cart {
constructor() { this.discount = 0.9; }
applyDiscount(items) {
return items.map((item) => item.price * this.discount);
// ↑ 箭头函数, this 继承外层(Cart 实例), 正确!
}
}
// (旧时代的另一种写法: 外层先 const self = this; 内层用 self.discount)
这个场景特别坑,因为它"半截是对的":外层方法的 this 是对的,可你一旦在里面写了个普通函数(数组的 map/forEach 回调、setTimeout 回调等),那个内层普通函数被调用时,this 又"重新按它自己的调用方式"来确定了——而它是被数组方法/定时器"独立调用"的,所以它的 this 又丢了,不会自动是外层的 this。这正是箭头函数大放异彩的地方:把内层回调写成箭头函数,它没有自己的 this、直接继承外层方法的 this——于是外层的 this 就能顺畅地"穿透"到内层回调里。(在箭头函数出现之前,人们用 const self = this 这种"把 this 存到一个变量里、内层用这个变量"的土办法来解决,你在老代码里还能看到 self/that 这种命名。)所以,在方法内部写回调函数时,默认就用箭头函数——它能自动帮你把外层的 this 带进来,省去无数 this 丢失的烦恼。把各种调用方式下 this 的指向整理成一张表:
| 调用方式 | this 指向 | 例子 |
|---|---|---|
| 对象方法调用 | 那个对象 | obj.fn() |
| 普通独立调用 | undefined(严格)/全局 | fn() / 摘下来的回调 |
| call/apply 调用 | 你指定的对象 | fn.call(obj) |
| bind 后调用 | bind 时绑定的对象 | fn.bind(obj)() |
| new 构造调用 | 新创建的实例 | new Fn() |
| 箭头函数 | 定义时外层的 this(不变) | () => this.x |
第五件事:箭头函数虽好,但别在这些地方用
箭头函数解决了 this 的大半烦恼,但"箭头函数没有自己的 this"这个特性,在某些场景下反而会"帮倒忙"。所以不能无脑全用箭头函数,有几个地方要避开。
// 误用1: 用箭头函数定义"对象的方法" —— this 不会指向该对象!
const obj = {
name: "obj",
greet: () => console.log(this.name), // ✗ 箭头函数的this是外层(可能是window), 不是obj!
};
obj.greet(); // undefined ! (不是 "obj") —— 对象方法别用箭头函数
// 正解: 对象方法用普通函数(或方法简写), this 才会指向该对象
const obj2 = {
name: "obj",
greet() { console.log(this.name); }, // ✓ "obj"
};
// 误用2: 需要用 this 指向"触发事件的DOM元素"时, 别用箭头函数
button.addEventListener("click", function () {
this.classList.add("active"); // ✓ 这里要的就是 this=button, 用普通函数
});
// 若用箭头函数, this 就不是 button 了
// 误用3: 原型方法、需要动态 this 的场景, 一般也不用箭头函数
这里的关键是理解:箭头函数的优势(this 固定为外层)在某些场景恰恰是它的劣势——当你"需要 this 动态地指向调用者"时,箭头函数的"固定 this"就帮了倒忙。典型的有几处:一是对象字面量的方法——如果用箭头函数定义,它的 this 会指向定义时的外层(通常是全局),而不是这个对象,导致 this.xxx 拿不到对象的属性;二是需要 this 指向 DOM 元素的事件处理——传统事件回调里 this 默认指向触发事件的元素(this.classList),这正是你想要的,用箭头函数反而拿不到。把"何时用箭头、何时用普通函数"整理成一张表:
| 场景 | 用哪种 | 原因 |
|---|---|---|
| 方法内的回调(map/setTimeout等) | 箭头函数 | 继承外层 this, 不丢失 |
| 类组件的事件处理方法 | 箭头函数(类字段)/bind | 锁定实例, 当回调传也不丢 |
| 对象字面量的方法 | 普通函数/方法简写 | 箭头会让 this 指向外层而非该对象 |
| 需 this 指向 DOM 元素的事件 | 普通函数 | 要的就是动态 this=触发元素 |
| 原型方法 / 需动态 this | 普通函数 | 需 this 随调用者变化 |
这张表的核心判断标准就一条:你这个函数,需要的是"固定的 this"(锁定某个对象,不随调用变)还是"动态的 this"(随调用者灵活变化)?需要固定的(回调里继承外层、方法当回调传)用箭头函数;需要动态的(对象方法、DOM 事件要指向触发元素)用普通函数。箭头函数不是"更高级、应该全用"的语法,而是"this 固定"这个特定行为的工具——用对场景它是利器,用错场景它是新坑。理解了它"this 固定"的本质,你就能在每个场景里,准确地判断该不该用它。
一张"this 该怎么处理"的决策图
把这次踩坑沉淀成一张图。每当你写一个用了 this 的函数、或要把方法当回调传时,照着它判断一下:
这张图把 this 的几种典型情况都覆盖了:会当回调传又要固定 this 的,用类字段箭头函数或 bind 锁死;方法内的回调,用箭头函数继承外层;对象方法/要 this 指向 DOM 元素的,用普通函数。核心判断永远是那个:你需要的是"固定的 this"还是"动态的 this"?把这个判断变成习惯,this 丢失这个坑就基本绝迹了。
我立下的几条 this 使用规矩
这次"方法当回调 this 就丢"的事故后,我给自己立了几条规矩:
- 记住 this 看调用方式:this 由"函数怎么被调用"决定,而非"在哪定义";方法被摘下来独立调用,this 就丢。
- 方法内回调默认用箭头函数:方法里写 map/setTimeout 等回调,默认用箭头函数,自动继承外层 this。
- 当回调的方法用类字段箭头函数:注定要当回调传的实例方法,用"类字段 + 箭头函数"定义,一次锁定 this。
- 对象字面量方法用普通函数:对象的方法别用箭头函数(this 会指向外层而非该对象)。
- 要 this=DOM 元素用普通函数:事件回调里要拿触发元素时用普通函数,别用箭头函数。
- 搞清 call/apply/bind:call/apply 立即调用指定 this,bind 返回 this 固定的新函数;按需选用。
- 遇到 this 是 undefined 先查调用方式:报"读取 undefined 的属性"且涉及 this 时,优先排查是不是 this 在传递中丢了。
这几条里,第二、三条几乎能消灭你日常工作中绝大多数 this 问题。我尤其想强调一个心态:不要试图去"记住所有 this 的规则然后逐条套用"(那太累、也容易记错),而要抓住那个唯一的本质——"this 由调用方式动态决定"——再从这个本质出发,去理解每一种情况。箭头函数为什么能解决问题?因为它打破了这个本质(它的 this 不由调用方式决定,而是固定为定义时的外层)。对象方法为什么不能用箭头函数?因为你恰恰需要 this 由调用方式决定(指向调用它的那个对象)。所有这些看似零散的规则,都能从"this 由调用方式决定"这一个本质推导出来——抓住这个本质,你就不用死记规则,而能临场推理出任何情况下 this 该是什么、该怎么处理。
写在最后:理解"为什么这么设计",比记住"是什么"更重要
这次被 this 折腾的经历,和我之前踩过的不少坑,在我心里指向了同一个学习方法上的领悟:面对一个让人困惑的、反直觉的语言特性(比如 JS 的 this),与其去死记硬背它林林总总的规则和无数的特殊情况,不如沉下心去搞懂它"为什么被设计成这样"、它背后那个统一的运作机制是什么——一旦抓住了那个本质,所有看似杂乱的规则,就都变成了从这个本质自然流淌出来的、可推理的结果。 this 的本质是"动态绑定、由调用方式决定",抓住了它,"方法摘出去就丢""箭头函数能锁定""对象方法别用箭头"……这些原本要死记的规则,就全都能从这一条本质里推导出来,不再需要硬背。
想通这一点,我对"学习一门技术"的方式,有了更深的体会。初学者容易陷入"记住一条条规则"的模式——this 在这种情况是这样、在那种情况是那样,列成一张长长的、要死记的清单;而真正的高手,追求的是理解那条贯穿所有规则的"主线"或"第一性原理",然后用它去推导一切。前者记住的是一堆离散、易忘、挂一漏万的点;后者掌握的是一条能生成所有点的、触类旁通的线。这两种学习方式的差距,会随着你遇到的情况越复杂、越边缘,而越拉越大——因为再长的规则清单也列不全所有情况,而一条正确的本质却能应对你从未见过的新情况。所以,每当你遇到一个让你困惑、需要死记的特性时,别急着去背规则,先停下来问一句:它为什么是这样的?背后那个统一的机制是什么?——把这个"为什么"想透,你就从"记住了一些规则"升级到了"真正理解了它"。
所以,如果你也在学 JavaScript、或任何一门有"反直觉特性"的语言,我想把这次踩坑最想说的话送给你:遇到 this 这样让你头疼的东西,别满足于"我记住了它在这几种情况下的表现",而要追问到它"为什么这样设计"的那个本质上去。因为记规则,你永远在被动地、零散地应付一个个具体情况,还总有记不全、记错的时候;而懂本质,你就能主动地、统一地推理出任何情况,以不变应万变。那个磨人的 this,最终教给我的,与其说是"this 的种种规则",不如说是一种学习的方法论——遇到困惑,向"为什么"深挖一层,去抓那条能解释一切的本质主线。这种"重理解、轻记忆,抓本质、推规则"的学习方式,价值远远超过了 this 这一个知识点本身;它是从"记住很多"到"真正懂得"的那把钥匙。愿你我都能少背一点规则,多懂一点本质,把每一个曾经困惑我们的特性,都变成手中清澈、可推理、用得游刃有余的工具。
—— 别看了 · 2026