一个把对象方法直接作为回调传给 setTimeout 的写法,执行时 this 变成了 undefined、访问 this 的属性全报错:一次 JavaScript this 绑定丢失的深度复盘

把对象的方法 handleClick(里面用了 this.count)作为回调传给 setTimeout,执行时报 Cannot read properties of undefined——this 居然是 undefined。根因是 JS 的 this 不由'函数在哪定义'决定、而由'如何被调用'决定:setTimeout(obj.fn) 只是把函数本身取出来传过去、和 obj 脱钩,之后被独立调用时 this 就是 undefined(严格)/window。本文讲透 this 的动态绑定规则和箭头函数 this 固定的特性,给出箭头函数包一层、bind、class 箭头字段保住 this 的正解,梳理 this 与函数常见坑,最后落到'this 的意义由调用上下文赋予、移动上下文相关的东西要带上它的上下文'的认知。

一个把对象方法直接作为回调传给 setTimeout 的写法,执行时 this 变成了 undefined、访问 this 的属性全报错:一次 JavaScript this 绑定丢失的深度复盘

那个 bug 是个经典的"this 不见了":我有一个对象,里面有个方法 handleClick,方法里用了 this.countthis.render()。我把这个方法作为回调传给了 setTimeout(和事件监听),想着到时候它会被调用、做它该做的事。可一运行就报错:Cannot read properties of undefined (reading 'count')——this 居然是 undefined。我很困惑:这方法明明是对象的方法,里面的 this 不就该是那个对象吗?怎么会是 undefined?我对着这段"逻辑上 this 显然该是对象"的代码排查了半天,才终于想起 JS 里 this 那个让无数人栽过跟头的规则,后背发凉:JavaScript 里,this 的值不是由"函数在哪里定义"决定的,而是由"函数如何被调用"决定的。当我写 setTimeout(obj.handleClick, 1000) 时,我其实只是把 handleClick 这个函数本身取出来、当作一个普通函数传了过去——它和 obj联系在这一刻就断了。等到 setTimeout 时间到了去调用它时,它是作为一个独立的、孤立的函数被调用的(obj. 这个前缀早就没了),没有任何对象在调用它;于是它内部的 this,在严格模式下就是 undefined(非严格模式下是全局对象 window)。问题的根,是 JS 的 this 是"动态绑定"的——谁调用它、它的 this 就是谁;我把方法从对象上"摘下来"单独传递,就切断了它和对象的绑定。这篇就把这次"this 绑定丢失"的坑,从头到尾复盘一遍。

故障现场:把方法摘下来当回调,this 丢了

问题代码,是一个把对象方法作为回调直接传递的写法:

const counter = {
  count: 0,
  handleClick() {
    this.count++;             // 用了 this
    console.log(this.count);
  },
};

// ✗ 出问题: 把方法直接作为回调传过去
setTimeout(counter.handleClick, 1000);   // ✗ this会丢!
// 或: button.addEventListener("click", counter.handleClick);  // 同样this会丢
// 或: [1,2,3].forEach(counter.handleClick);                    // 同样

// 1秒后报错: Cannot read properties of undefined (reading 'count')
//   → 因为调用时 this 是 undefined(严格模式)

// 为什么 this 丢了:
// - JS的this【不由函数定义位置决定, 而由"函数如何被调用"决定】(动态绑定);
// - counter.handleClick() 这样调用: 是"counter在调用", this = counter(对的);
// - 但 setTimeout(counter.handleClick, 1000): 这里只是把 handleClick这个【函数本身】传过去,
//   "counter." 这个前缀只是用来【取出】这个函数, 取出后函数和counter就【没关系了】;
// - 1秒后setTimeout去调用它时, 是【作为一个独立函数调用】(没有任何对象 . 它);
//   → this = undefined(严格模式) / window(非严格);
// - → this.count → undefined.count → 报错。

// this绑定规则简记(看"怎么调用"):
// - obj.fn()        → this = obj(谁点出来的就是谁);
// - fn()            → this = undefined(严格)/window(非严格)(独立调用, 没有调用者);
// - new Fn()        → this = 新创建的对象;
// - fn.call(x)/apply/bind(x) → this = x(显式指定);
// - 箭头函数        → this = 定义时所在作用域的this(不看怎么调用!)。

// 关键: JS的this是动态的, 取决于"怎么调用"而非"在哪定义"; 把对象方法摘下来单独传递/调用,
//       就切断了它和对象的绑定, this会丢(变undefined/window)。

第一次彻底搞懂 this 时,我又恍然又感慨:"我一直以为 this 就是'当前这个对象',原来它是'谁调用我我就是谁',是动态变的。"这个坑最违反直觉的地方在于:大多数语言里(Java/C++/Python 的 self),"方法里的 this/self"是和"方法所属的对象"绑定死的;可 JS 偏不——JS 的 this 是"动态的",同一个函数,用不同的方式调用,this 可以完全不同而"把方法当回调传递"(setTimeout、事件监听、数组方法、Promise.then)是极其常见的操作,每一次都可能不经意地切断 this 绑定,所以这个坑出现的频率非常高。下面就来拆解,this 的规则和怎么保住绑定。

第一件事:搞懂 JS 的 this 绑定规则

我认真重学了 JS 的 this,才彻底理解这个坑和正解。

JavaScript 的 this 绑定规则: 看"怎么调用", 不看"在哪定义"

【核心: 普通函数的this在【调用时】根据调用方式确定; 把方法摘下来单独调用就丢绑定; 箭头函数的this是定义时的(不变)】

1. this 是"动态绑定"的(普通函数):
   - this 的值【不在函数定义时确定】, 而在【每次调用时】根据"怎么调用"确定;
   - 同一个函数, 不同调用方式, this 不同。

2. 普通函数的this规则(按优先级):
   - new Fn():           this = 新建的对象;
   - fn.call/apply/bind: this = 你显式指定的那个;
   - obj.fn():           this = obj(谁"点"出来调用的, 就是谁);
   - fn()(独立调用):    this = undefined(严格模式) / 全局对象(非严格);
   - → "把方法摘下来单独调"(回调、赋值给变量再调), 就落到最后一种 → this丢失。

3. 箭头函数的this【不一样】(关键!):
   - 箭头函数【没有自己的this】; 它的this = 【定义它时, 外层作用域的this】;
   - 且这个this【一旦定义就固定】, 不随调用方式改变(也不能被call/bind改);
   - → 这正是用箭头函数"保住this"的原理: 它捕获了定义时的this。

4. 为什么"传方法作回调"会丢this:
   - setTimeout(obj.fn) / arr.forEach(obj.fn) / el.onclick = obj.fn:
   - obj.fn 只是【取出函数】, 传过去的是"裸函数", 和obj脱钩;
   - 之后由 setTimeout/forEach/事件系统去【独立调用】它 → this按"独立调用"规则 → 丢。

5. 类比: this像"代词''"——""指谁, 取决于【是谁在说这句话】(怎么调用),
   而不是【这句话写在哪本书里】(在哪定义); 你把一句带""的话抄给别人去念,
   ""就变成念的人了(this变了)。

一句话: JS普通函数的this由"调用方式"动态决定(不是定义位置); 把方法摘下来单独调用会丢this;
   箭头函数的this是定义时外层的this、固定不变——这是保住this的关键工具。

这套规则,是整个坑的根。this 是动态绑定的(普通函数):它的值不在定义时确定、而在每次调用时根据"怎么调用"确定,同一函数不同调用方式 this 不同。普通函数的 this 规则(按优先级):new Fn()→新对象、call/apply/bind→显式指定、obj.fn()→obj(谁点出来调用的)、fn() 独立调用→undefined(严格)/全局——把方法摘下来单独调就落到最后一种、this 丢失箭头函数的 this 不一样(关键):它没有自己的 this,this = 定义它时外层作用域的 this、且固定不变(不随调用方式改、也不能被 bind 改)——这正是用箭头函数保住 this 的原理。就像this 像代词"我"——"我"指谁取决于是谁在说这句话(怎么调用),而非这句话写在哪本书里(在哪定义);你把带"我"的话抄给别人念,"我"就变成念的人了一句话:JS 普通函数的 this 由"调用方式"动态决定(不是定义位置);把方法摘下来单独调用会丢 this;箭头函数的 this 是定义时外层的 this、固定不变——这是保住 this 的关键工具。

第二件事:正解——用箭头函数包一层、bind、或 class 箭头字段

搞懂了原理,正解就清晰了:传回调时用箭头函数包一层(保住调用形式)、用 bind 显式绑定、或在 class 里用箭头函数字段定义方法;让方法被调用时 this 仍指向对象

const counter = {
  count: 0,
  handleClick() {
    this.count++;
    console.log(this.count);
  },
};

// ====== 正解一: 用箭头函数包一层(保住 obj.fn() 的调用形式) ======
setTimeout(() => counter.handleClick(), 1000);
//          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 箭头函数里【显式地 counter.handleClick()调用】
// → 真正调用时是 counter.handleClick(), this = counter, 正确!
//   箭头函数本身的this是定义时的(这里无所谓), 关键是它内部"counter.fn()"这个调用形式保住了。

// ====== 正解二: 用 bind 显式绑定 this ======
setTimeout(counter.handleClick.bind(counter), 1000);
//                            ^^^^^^^^^^^^^^^ bind返回一个"this永久绑定为counter"的新函数
// → 无论怎么调用, 这个绑定后的函数 this 都是 counter。
// 事件监听同理: el.addEventListener("click", counter.handleClick.bind(counter));

// ====== 正解三(class场景, 推荐): 用箭头函数字段定义方法 ======
class Counter {
  count = 0;
  // ★ 用箭头函数定义为类字段: this在定义时就绑定为实例, 不随调用方式变
  handleClick = () => {
    this.count++;
    console.log(this.count);
  };
}
const c = new Counter();
setTimeout(c.handleClick, 1000);   // ✓ this依然是c(箭头字段绑定了)
button.addEventListener("click", c.handleClick);  // ✓ 也没问题
// → React等框架里, 类组件的事件处理常用这种箭头字段写法来保住this(或在constructor里bind)。
# ====== 保住this的几种方式对比 ======
# - 箭头函数包一层 () => obj.fn():  简单直接, 保住"obj.fn()"调用形式; 最常用;
# - bind: obj.fn.bind(obj):         返回this永久绑定的新函数; 适合传递时;
# - class箭头字段 fn = () => {...}:  类里定义方法时this自动绑实例; React类组件常用;
# - 在外层存 const self = this:      老写法(箭头函数普及前), 用闭包变量保住this;

# ====== 关键判断: 看这个函数"最终会怎么被调用" ======
# - 如果会被"摘下来单独调用"(作回调) → this会丢 → 要用上面的方式保住;
# - 如果总是 obj.fn() 这样调用 → this没问题。

# ====== 易混点 ======
# - 不是所有函数都该用箭头函数! 箭头函数的this是"定义时外层的",
#   对象方法/原型方法如果需要this指向"调用它的对象", 反而【不能】用箭头函数定义。
#   (箭头函数适合"回调、需要捕获外层this"的场景; 对象方法本身用普通函数+正确调用)

# 核心: 传方法作回调时, 用箭头函数包一层 (()=>obj.fn()) 或 bind 保住this; class里用箭头字段;
#   判断"这函数会被怎么调用", 会被摘下来单独调就要保this; 箭头函数this固定是定义时的, 按需用。

修复的核心,是"让方法被调用时,this 仍能指向对象"正解一:用箭头函数包一层——() => counter.handleClick(),真正调用时是 counter.handleClick() 的形式、this 正确;最简单常用正解二:用 bind——counter.handleClick.bind(counter) 返回一个 this 永久绑定为 counter 的新函数,无论怎么调用 this 都对正解三(class 推荐):箭头函数字段——handleClick = () => {...},this 在定义时绑定为实例、不随调用方式变(React 类组件常用)关键判断:看这个函数"最终会怎么被调用"——会被摘下来单独调用(作回调)就会丢 this、要保住;总是 obj.fn() 调用就没问题易混点:不是所有函数都该用箭头函数!对象方法若需要 this 指向调用它的对象,反而不能用箭头函数定义(箭头函数 this 是定义时外层的)归根结底:传方法作回调时用箭头函数包一层或 bind 保住 this;class 里用箭头字段;判断"这函数会被怎么调用",会被摘下来单独调就要保 this;箭头函数 this 固定是定义时的、按需用。

第三件事:JavaScript this 与函数相关的其他常见坑

排查后我把 this 和函数相关的其他常见坑也系统梳理了一遍。

JavaScript this / 函数的其他常见坑

# 1. 方法作回调丢this(本文): 摘下来单独调用this丢。→ 箭头包一层/bind/class箭头字段。

# 2. 该用普通函数处却用箭头函数: 对象方法用箭头, this成了外层(常是window)而非对象。→ 对象方法用普通函数。

# 3. 嵌套函数的this: 方法里又定义普通function, 它的this不是外层对象。→ 内层用箭头函数捕获this。

# 4. 把bind的结果再bind: bind后的函数this已固定, 再bind无效。→ 注意bind只生效一次。

# 5. 事件处理器里的this: 普通function的事件处理器this是触发元素; 用箭头函数则是外层。→ 看需求选。

# 6. setTimeout/setInterval回调this: 同本文, 传方法要保this。

# 7. 数组方法的thisArg: forEach/map等第二个参数可传thisArg指定回调的this。

# 8. 闭包变量 vs this: 老代码用 const that=this 闭包保this; 现代用箭头函数更简洁。

# 共同根源: JS的this是"动态的、调用时决定的", 这和多数语言(this绑定到实例)不同;
#   不理解"this取决于怎么调用"、以及"箭头函数this是定义时固定的", 就会在回调/嵌套里丢失或搞错this。

# 核心: 理解this由调用方式决定、箭头函数this是定义时外层的; 传方法作回调用箭头/bind保this;
#   对象方法用普通函数、回调用箭头函数; 想清楚"这函数会被怎么调用、this该是谁"。

排查让我把 this 的其他坑也梳理清了。一、方法作回调丢 this(本文)。二、该用普通函数处用了箭头函数(对象方法 this 成外层)。三、嵌套函数的 this(内层用箭头捕获)。四、把 bind 的结果再 bind(无效)。五、事件处理器里的 this六、setTimeout 回调 this七、数组方法的 thisArg八、闭包变量 vs this它们的共同根源是:JS 的 this 是"动态的、调用时决定的",这和多数语言(this 绑定到实例)不同;不理解"this 取决于怎么调用"以及"箭头函数 this 是定义时固定的",就会在回调/嵌套里丢失或搞错 this核心是:理解 this 由调用方式决定、箭头函数 this 是定义时外层的;传方法作回调用箭头/bind 保 this;对象方法用普通函数、回调用箭头函数;想清楚"这函数会被怎么调用、this 该是谁"下面这张图,是这次 this 丢失坑的成因与解法:

第四件事:this 取值速查表(按调用方式)

这次踩坑后,我把"不同调用方式下 this 是什么"整理成一张表,一查就清楚。

调用方式 this 是 说明
obj.fn() obj 谁"点"出来调用就是谁
fn()(独立调用) undefined(严格)/window 没有调用者, this丢(本文)
new Fn() 新建的对象 构造函数
fn.call(x)/apply/bind(x) x 显式指定
箭头函数 定义时外层的this 固定, 不随调用变
DOM事件处理(普通函数) 触发事件的元素 addEventListener的回调

这张表把 this 的取值钉清了。核心是:判断一个普通函数里 this 是什么,不看它写在哪,而看它"是被怎么调用的"——有没有 obj. 前缀、是不是 new、有没有 call/bind;而箭头函数是唯一的例外:它的 this 看"定义在哪"(外层作用域),固定不变它给我的最大启发是:this 的复杂,本质是因为它是一个"上下文相关"的、随环境变化的东西——它不像普通变量那样"值是确定的",而是"在不同的调用上下文里有不同的值";理解这类"上下文相关"的东西,关键是搞清"它的值由什么上下文决定"(this 由"调用方式"这个上下文决定)这其实是理解很多"动态/上下文相关"特性的钥匙:编程里有不少东西的值/行为是"依赖上下文"的(this、闭包捕获的变量、动态作用域、依赖注入的实例、ThreadLocal)——对它们,不能用"静态地看定义"的思路去理解,而要问"这个具体的运行上下文里,它此刻是什么";"分清'静态确定'和'上下文动态确定'的东西、并搞清后者的决定因素",是理解这类特性的关键用"看调用方式"判断 this、理解上下文相关特性的决定因素——是这个坑带给我的认知。

第五件事:箭头函数不是"更好的普通函数"

这次也让我厘清:箭头函数和普通函数各有用途,不能无脑全用箭头。我对比成表。

维度 普通函数 箭头函数
this 调用时动态决定 定义时外层的, 固定
适合 对象方法/需this指向调用者 回调/需捕获外层this
能否被bind改this 不能
有arguments吗 没有(用外层的)
能当构造函数new吗 不能

这张表道出了一个常见的误区。核心是:箭头函数不是"普通函数的升级版/更好版",而是"this 行为不同的另一种函数"——它的 this 固定为定义时外层的、不随调用变;这在"回调、需要捕获外层 this"时是优点(本文保 this 就靠它),但在"对象方法、需要 this 指向调用它的对象"时是缺点(用箭头函数 this 反而错了)它给我的深刻启发是:很多语言特性是"各有适用场景的工具",而不是"谁绝对优于谁"——箭头函数和普通函数,不是"新的取代旧的",而是"各管一摊、各有所长";"无脑全用新特性"(凡函数皆箭头)和"固守旧的"一样,都是没理解它们差异的表现——正确的做法是理解它们的差异、按场景选用这给了我一种使用语言特性的成熟态度:面对"新旧两种做同类事的特性"(箭头/普通函数、let/var、class/原型、async/Promise),不要简单地认为"新的就该全用",而要搞清它们的具体差异和各自适用的场景——"它们差在哪、各适合什么",比"哪个更新"重要得多;"理解差异、按场景选用",而非"跟风全换新的",才能真正用对工具认清箭头函数不是普通函数的升级而是各有场景、按差异选用语言特性——是这个坑带给我的认知。

第六件事:把方法当回调传时,我现在的检查习惯

现在每当我要把一个方法作为回调传出去,我都会按这张图先想一想:

这张图的精髓,是"用了 this 的方法要当回调传,就用箭头包一层或 bind 保住 this"函数用到 this会被摘下来单独调用(setTimeout/事件/数组方法),this 就会丢、必须保:箭头函数包一层、bind、或 class 箭头字段这套习惯,让我从"方法随手传作回调"变成了"传前先想它用没用 this、会被怎么调用"——核心始终是:用了 this 的方法当回调传会丢 this,用箭头包一层或 bind 保住。

我立下的几条规矩

这场"this 绑定丢失"的事故,换来了我写 JavaScript 时,刻进骨子里的几条铁律:

  1. JS 的 this 由"怎么调用"决定,不由"在哪定义"。它是动态绑定的。
  2. 把方法摘下来单独调用(作回调),this 会丢。变 undefined/window。
  3. 保 this:箭头函数包一层、bind、或 class 箭头字段。
  4. 箭头函数的 this 是定义时外层的,固定不变。这是它保 this 的原理。
  5. 对象方法别用箭头函数定义。否则 this 不指向对象而是外层。
  6. 传方法前先想"它会被怎么调用、this 该是谁"。会被摘下来调就保 this。
  7. 箭头/普通函数各有场景,按差异选用。别无脑全用箭头。

写在最后

回头看,这场由"把方法当回调传、this 丢了"引发的事故,真正教给我的,远不止"用箭头函数或 bind 保 this"这一个技巧。它让我对"有些东西的'含义',不是固定的,而是由它'所处的上下文'决定的;脱离了原来的上下文,它的含义就变了",有了一次刻骨的体会。我栽跟头,根源在于我把 this 当成了一个"固定指向那个对象"的东西——就像我以为"方法里的 this,永远是这个方法所属的对象",这个绑定是写死的、跟着方法走的。可 JS 的 this 偏偏是"上下文相关"的:它的含义,取决于方法"此刻被调用的那个上下文"(谁在调用它);当我把方法从对象上"摘下来"、放到 setTimeout 的上下文里去调用时,它脱离了"obj 在调用"这个原来的上下文,this 的含义自然就跟着那个新上下文变了(变成了"没有调用者");我以为 this 是"随方法走的固定标签",实际它是"随调用上下文变的活指针"这让我领悟到一个普适的认知:很多东西的"意义"是"上下文赋予"的,而非"自身固有"的——this 的意义由调用上下文赋予、一个词的意义由语境赋予、一段代码的行为由它运行的环境赋予、一个数据的含义由它的上下文(时区/单位)赋予;把这些"上下文相关"的东西从原上下文里抽离、放到新上下文里时,它的意义/行为很可能就变了——而我们常常误以为它会带着原来的含义一起搬过去这给了我一种处理"上下文相关"事物的警觉:当你"移动、传递、复用"一个上下文相关的东西时(把方法当回调传、把代码片段挪到别处、把数据传到另一个系统),要特别留意"它脱离原上下文后,含义/行为还和原来一样吗"——"它依赖的那个上下文,还在吗?跟过去了吗?";"意识到一个东西是'上下文相关'的、并在移动它时主动地把它需要的上下文也一起带上(或重新建立)"——这是避开一大类"换了环境就出错"问题的关键认清 this 等事物的意义由上下文赋予、移动上下文相关的东西时要带上它的上下文——这,是我用一次 this 丢失的事故,换来的、关于 JavaScript、也关于如何理解一切上下文相关事物的、最朴素也最深刻的领悟。如果这篇复盘,能让你下次把一个用了 this 的方法当回调传出去时,顺手用箭头函数包一层,那我对着那个 undefined 的 this 排查的这段时间,就值了。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

一段用浅拷贝复制配置模板的 Python 代码,我改了副本里的一个嵌套列表,结果把原始模板和其他所有副本一起改了:一次浅拷贝陷阱的深度复盘

2026-6-2 18:35:38

技术教程

一个每次请求都起一个 goroutine 却没人保证它能退出的服务,goroutine 越积越多、内存缓慢上涨,跑几天就 OOM:一次 goroutine 泄漏的深度复盘

2026-6-2 18:45:38

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索