故障排查 · 2026 年 11 月 11 日

JavaScript 的 this 到底指向谁:六种绑定一次讲清

同一个方法,直接调用正常,作为回调传给 setTimeout 就报 undefined。this 由调用点决定,不是定义位置。本文用六种绑定、一张优先级表和可运行的例子把这件事定死。

同一段代码,user.greet() 返回 hi ada,把 user.greet 传给 setTimeout 就变成 hi undefined,严格模式下直接抛 TypeError。

很多人背过「this 指向调用它的对象」,但这句话只对了一半。this 在函数被调用时确定,取决于调用点的形式,和函数写在哪里基本无关——箭头函数是唯一的例外。理解这一点,大部分「this 是 undefined」的报错都能当场解释。

现象:同一个函数,三种结果#

javascript
const user = {
  name: 'ada',
  greet() {
    return `hi ${this.name}`;
  },
};

console.log(user.greet());        // 'hi ada'
const g = user.greet;
console.log(g());                 // 非严格:'hi undefined';严格/模块:TypeError

setTimeout(user.greet, 0);        // 回调里的 this 已不是 user

把方法取出来单独调用,就丢掉了「谁在调用」这条信息。事件监听、数组遍历、Promise 回调都是同一个道理。

事件监听是第二个高发场景:el.addEventListener('click', this.handleClick) 里,普通函数的 this 是那个 DOM 元素;数组遍历也一样,items.map(obj.format) 的 this 是 undefined。只要方法离开了「被哪个对象调用」的位置,this 就断了线索。

第三个场景是 class。把 counter.inc 解构出来赋值给变量、再作为回调传出去,this 就没了;报错信息通常是 Cannot read properties of undefined (reading 'count'),看起来像字段没初始化,实际是 this 不对。

Promise 链也常中招:promise.then(obj.handle),then 只负责传值,不负责绑定接收者。需要 this 时写成 promise.then((v) => obj.handle(v)),行为才确定。

定位:先判断三件事#

第一,看调用形式:是 fn() 还是 obj.fn()?有没有经过 call、bind、new?

第二,看函数是不是箭头函数。箭头函数没有自己的 this,bind 也改不了它。

第三,看运行在严格模式还是模块里。ES Module 和 class 内部默认严格,此时默认绑定的 this 是 undefined,不是全局对象。

运行时可用的几个探针:

javascript
function probe() {
  console.log(this);                          // 先看实际值
  console.log(typeof this, this?.constructor?.name);
}
probe();                    // undefined(模块中)或 globalThis(脚本中)
probe.call({ a: 1 });       // { a: 1 }

如果 this 是 undefined,报错通常是 Cannot read properties of undefined;如果是全局对象,症状是不报错但读到 undefined 字段——后者更难查,因为代码不崩,只是数据不对。

在浏览器里更快的方式是断点:在可疑函数第一行下断点,看右侧 call stack 里的调用形式,obj.fn() 和 fn() 长得完全不一样。再配合 console.trace(),能直接看到是谁调用的。

顺带记一下顶层 this:模块顶层的 this 在 ESM 里是 undefined,在 CommonJS 里是 module.exports,在普通 script 里是 window。同一段代码换个加载方式,结果就变了。

根因:六种绑定与一条优先级#

调用形式 this 指向 说明
fn() undefined(严格/模块)或 globalThis 默认绑定
obj.fn() obj 隐式绑定,看调用点左侧
const g = obj.fn; g() 同默认绑定 隐式丢失,最常见的坑
fn.call(x) / apply / bind x 显式绑定,bind 生成新函数且不可再改
new Fn() 新创建的实例 new 绑定,优先级最高
() => this 外层词法作用域的 this 箭头函数,无法被改变

优先级从高到低:new > bind > call/apply > 隐式 > 默认。一个直观的验证:

javascript
function Person(name) { this.name = name; }

const Bound = Person.bind({ name: 'bound' });
console.log(new Bound('ada').name);   // 'ada',new 忽略了 bind 的 this

const arrow = () => this;
console.log(arrow.call({ a: 1 }) === this);   // true,箭头函数不认 call

new (() => {})();                     // TypeError: not a constructor

机制上可以这么理解:obj.fn() 在规范里叫「引用」,调用时会把 obj 作为 this 传入。一旦把这个引用赋给变量、作为参数传出去,引用就退化成普通值,this 的线索随之丢失。(obj.fn)() 因为括号不改变引用,仍然是隐式绑定;而 (0, obj.fn)() 经过逗号运算符,引用就没了。

严格模式是另一个变量。非严格模式下,默认绑定的 this 会被替换成全局对象,于是 this.name 读到 undefined 而不报错;严格模式下保持 undefined,调用 this.xxx 立即抛错。同一份代码在不同打包配置下表现不同,多半就是这个原因。

提示

class 内部默认严格模式,所以 const inc = counter.inc; inc() 不会静默读到全局变量,而是直接抛 TypeError。这算是好事:错误更早暴露。

修复:四种写法,各有取舍#

javascript
const user = { name: 'ada', greet() { return `hi ${this.name}`; } };

// 1) bind:生成一个 this 固定的新函数
const bound = user.greet.bind(user);

// 2) 箭头包装:延迟到调用时再取 user.greet
setTimeout(() => user.greet(), 0);

// 3) 显式传入:适合工具函数
function greetWith(ctx) { return `hi ${ctx.name}`; }
greetWith(user);

// 4) class 字段箭头:实例方法自带 this,适合事件处理器
class Counter {
  count = 0;
  inc = () => { this.count += 1; return this.count; };
}
const c = new Counter();
const inc = c.inc;
inc();                       // 1,不再依赖调用点

取舍很清楚:bind 在构造函数里绑一次,开销小,但容易被忘记;class 字段箭头每个实例都创建一份函数,实例多时内存占用更高;箭头包装每次调用都新建闭包,适合回调但别放进热循环。

事件处理器用箭头函数最省事:el.addEventListener('click', () => this.handleClick()),this 来自定义处,与谁触发无关。反过来,如果确实需要触发元素,用普通函数配合 event.currentTarget,别依赖 this。

框架之间的差异也要记住:Vue 的 methods 会自动绑定到组件实例,React class 组件不会,必须自己 bind 或用箭头字段。跨框架迁移时,这类问题会成片出现。

还有一条现代替代:不再用 const self = this 这种写法,直接用箭头函数捕获外层 this,语义更清楚,也少一个变量。

call 与 apply 的区别只在传参:前者逐个传,后者传数组;bind 不执行函数,只返回一个新函数。临时借用一次用 call,需要长期固定 this 才用 bind。

防复发#

  • ESLint 打开 no-invalid-this,能拦住把 this 用在普通函数里的写法。
  • TypeScript 项目打开 @typescript-eslint/unbound-method,它会直接报「方法被解构后丢失 this」。这条规则依赖类型信息,配置成本略高,但收益最大。
  • 单测里专门构造一次「把方法当回调传出去」的调用,断言不抛错。这一步比读代码有效。
  • 事件监听、定时器、Promise 链是三个高发位置,review 时重点看这里是否直接传了 this.xxx。
  • 公共方法如果需要被外部当回调使用,就在类里同时提供绑定好的箭头版本,别让调用方猜。
  • 回调密集的模块(事件总线、定时任务、Promise 链)集中做一次 review,比逐处猜更有效。

结论#

this 由调用点决定:fn() 走默认绑定,obj.fn() 走隐式绑定,call/apply/bind 走显式绑定,new 走构造绑定,箭头函数则完全看外层作用域。

优先级记成一句话:new 最高,bind 次之,call/apply 再次,然后是隐式,最后是默认。

最常见的 bug 不是「this 指错了」,而是「方法被取出来当回调,隐式绑定丢了」。修法优先用箭头包装或 class 字段箭头,配合 unbound-method 这条 lint 规则,基本可以把它挡在上线之前。

最后提醒一句:this 是运行时概念,类型系统只能帮你到「这个方法可能被解构」这一层。真正兜底的,还是那句「谁在调用」——写回调时多问一遍,比事后调试省事。

箭头函数也不能当构造函数,没有 arguments,选它之前先确认这两点不影响使用场景。

完

#JavaScript#前端#语言特性
回到顶部 ↑