K 的一隅

JavaScript JavaScript 核心机制

同一个函数,为什么 this 会变:调用方式与绑定规则

从会议室话筒与点名发言说起,弄清 this 的四种绑定、call/apply/bind、箭头函数与 class 方法里的常见陷阱。

10 分钟阅读 更新于 2026-07-28
本文目录

会议室里同一支话筒,谁被点名谁发言。话筒没有固定主人;“这次谁在说”,取决于主持人把话筒递给了谁,而不是话筒上贴了谁的名字。

this 也类似:同一个函数体,每次调用时“当前这次执行代表谁”,不由函数写在哪个文件里决定,而主要由调用方式决定。第二章讲的是名字沿词法环境向外找;这一章讲的是“这次调用里的 this 绑给了谁”。

第二章区分过闭包与 this,这里把规则摊开

闭包回答:内层函数还能读到哪些词法绑定this 回答:这次函数执行时,当前对象上下文是谁。两者都挂在执行上下文上,但查找规则完全不同。

别急着背“箭头函数没有 this”这种口诀。先记住分工:变量名走词法链;this 走绑定规则(箭头函数例外,它从定义处捕获外层的 this)。

先猜输出:同一个函数,三次调用三种 this

js
const desk = {
  name: '主讲人',
  speak() {
    console.log(this.name)
  },
}

const fn = desk.speak

desk.speak() // ?
fn()           // ?
;(desk.speak)() // ?

第一行通常打印 主讲人:通过 desk.speak() 调用,this 指向 desk

第二行在浏览器非严格模式下可能打印 window(或 undefined 于严格模式):fn 只是函数引用,调用时没有“对象点号”那一侧,this 走默认绑定。

第三行仍打印 主讲人:括号只影响求值,不改变“通过对象属性访问再调用”的事实。

把这个实验记住,后面事件监听、setTimeout 丢回调、Vue 选项里方法被解构,很多“this 丢了”都来自同一类调用方式变化。

四种绑定:默认、隐式、显式、new

默认绑定:独立函数调用。非严格模式下,普通函数的 this 往往是全局对象;严格模式下是 undefined

js
function report() {
  'use strict'
  console.log(this)
}

report() // undefined

隐式绑定obj.method() 形式,this 指向点号左侧对象。

js
const cart = {
  items: 2,
  total() {
    return this.items * 10
  },
}

cart.total() // 20

但若你把 total 赋给变量再调,隐式绑定会丢失,回到默认绑定。

显式绑定callapplybind 强行指定 this

js
function greet(role) {
  console.log(`${role}${this.shop}`)
}

const ctx = { shop: '深夜面馆' }

greet.call(ctx, '掌柜')
greet.apply(ctx, ['伙计'])

const bound = greet.bind(ctx, '外送')
bound()

call 立即调用并传参;apply 用数组传参;bind 返回一个 this 已固定的新函数,稍后调用。

new 绑定new Fn() 会创建新对象,并把构造过程中的 this 绑到该对象上(与 prototype 的完整关系在第十二章展开)。

js
function Ticket(id) {
  this.id = id
}

const t = new Ticket(7)
console.log(t.id) // 7

判断一次调用时,可以按优先级想:是否 new?是否 call/apply/bind?是否 obj.fn()?否则多半是默认绑定。

箭头函数:捕获词法 this,而不是每次重新绑

js
const timer = {
  seconds: 0,
  start() {
    setInterval(() => {
      this.seconds += 1
      console.log(this.seconds)
    }, 1000)
  },
}

箭头函数没有自己的 this 参数,它会捕获定义时外层词法环境的 this。因此回调里仍指向 timer

若改成普通函数:

js
setInterval(function () {
  this.seconds += 1 // 非严格模式下 this 往往是 window
}, 1000)

this 会在每次回调调用时重新按默认规则绑定,通常不再是 timer

类字段箭头属性也常用于“把方法当回调传递仍保留实例”:

js
class Panel {
  label = '订单'

  render = () => {
    console.log(this.label)
  }
}

这是语法层面的便利,本质仍是词法捕获。不要把它和普通原型方法混为一谈。

class、事件监听与容易丢 this 的写法

js
class Order {
  constructor(id) {
    this.id = id
  }

  print() {
    console.log(this.id)
  }
}

const o = new Order(3)
const p = o.print
p() // 默认绑定,this 不是 o

在 DOM 事件里同样常见:

js
button.addEventListener('click', desk.speak) // 可能丢 this
button.addEventListener('click', () => desk.speak()) // 通过 desk 显式调用,保留 desk
button.addEventListener('click', desk.speak.bind(desk)) // 显式 bind

读框架源码或写组件时,看到方法被解构、被当作回调传递,先问:这次调用还是 obj.method() 吗?

严格模式、模块与全局 this 的收缩

js
function show() {
  'use strict'
  console.log(this)
}

show() // undefined,不再是 window

ES 模块顶层默认严格模式,顶层 this 通常是 undefined,而不是 window。这会影响你把普通函数当回调直接传递时的默认绑定。

在浏览器里,全局对象可通过 globalThis 访问。不要把“非严格默认绑定到 window”当成可靠特性写进库代码;显式传参或 bind 更清晰。

借方法调用:数组、类数组与函数借用

js
const data = { 0: 'a', 1: 'b', length: 2 }

Array.prototype.push.call(data, 'c')
console.log(data.length) // 3

push 本是为数组写的,但通过 callthis 指到类数组 data 上,就能复用同一套实现。这再次说明:函数体与 this 指向的对象可以分离

Function.prototype.call / apply 也是可借用的:

js
function tag(line) {
  return `[${this.prefix}] ${line}`
}

tag.call({ prefix: '会务' }, '开始了')

读源码时看到 xxx.call(yyy),先找:这次要把 this 临时指给谁?

Vue 选项式 API 里常见的 this 丢失

js
export default {
  data() {
    return { count: 0 }
  },
  methods: {
    inc() {
      this.count += 1
    },
    register() {
      bus.on('tick', this.inc) // 危险:inc 作为回调,默认绑定
    },
  },
}

bus.on('tick', this.inc) 把方法当值传递,调用时不再经过 this.inc() 的隐式绑定。修复可以是箭头函数包一层、bind,或在组合式 API 里根本不必依赖实例 this

组合式 setup 返回的函数闭包的是响应式引用,不是选项式那套 this 绑定。读 Vue 3 代码时要分清文件用的是哪一代写法。

优先级小结:从调用表达式倒推

遇到 this 困惑,按这个顺序问:

  1. 是否 new 调用?
  2. 是否通过 call / apply / bind 创建或调用?
  3. 是否 obj.method() 形式?
  4. 是否箭头函数(若是,去看定义处外层 this)?
  5. 否则按默认绑定,并确认是否在严格模式或模块中。

把这个流程写成便签贴在显示器旁,比背十张表格更快。调试时配合 console.log(this) 与调用栈顶部的调用表达式,一般两轮就能定位。

容易踩的坑:别把 this 当成闭包里的变量

  1. “函数在哪定义,this 就在哪”——那是箭头函数的行为,不是普通函数的规则。
  2. this 像第一个参数那样由调用者自动传入”——只有 call / apply / bind 才会显式指定;普通调用不会。
  3. this.xxx 和闭包捕获的 xxx 混用——词法变量来自环境链,this 来自当前调用的绑定。类里 this.count 是实例属性;外层 const count 与它无关,除非你主动赋值。
  4. 以为 bind 之后还能被 call 改掉——绑定函数通常已固定 this(用 new 调用绑定函数是另议边界)。
  5. 以为 obj?.method() 会改变绑定规则——可选链只短路访问;一旦真的执行 method(),隐式绑定仍指向 obj
  6. class 字段里写普通函数属性,却期待它与原型方法一样共享:
js
class Bad {
  handler = function () {
    console.log(this)
  }
}

每个实例都会得到自己的 handler 函数对象,内存与 this 行为都与原型方法不同。这是刻意的实例字段,不是“写错了”。弄清要共享行为(放 prototype / class 方法体)还是每实例一份(字段初始化器),再选写法。

Promise 与 async 不会自动绑定 this

js
const repo = {
  items: [1, 2, 3],
  async load() {
    await Promise.resolve()
    console.log(this.items)
  },
}

const fn = repo.load
fn() // 默认绑定,this 不是 repo

await 只暂停当前 async 函数,不会替你修复 this。若把 repo.load 交给路由钩子或事件总线,仍要 bind 或箭头包装。第十章讨论的是执行权何时恢复;恢复后 this 仍按本章规则来。

与闭包对照:同一回调里的两个维度

js
const app = {
  title: '订单面板',
  items: [1, 2, 3],
  render() {
    this.items.forEach(function (n) {
      console.log(this.title, n) // 普通函数回调:this 不是 app
    })
    this.items.forEach((n) => {
      console.log(this.title, n) // 箭头回调:this 来自 render 的 app
    })
  },
}

forEach 的回调是普通函数时,每次迭代都会按默认或严格规则绑定 this,与外层 renderthis 无关。箭头回调则捕获 render 执行时的 this。读这类代码要同时看:词法上能读哪些变量(闭包)与 这次函数体的 this 是谁

第一章说执行上下文同时包含词法环境与 this 绑定;第二章与第三章只是把两块拼图拆开。面试若问“箭头函数为什么没有 arguments / this”,答案都应回到“它复用外层的词法环境”。

手写 bind 的极简版:看清固定 this 的本质

js
function myBind(fn, thisArg, ...preset) {
  return (...args) => fn.apply(thisArg, [...preset, ...args])
}

const desk = { name: '夜班主持' }
function announce(item) {
  console.log(`${this.name}${item}`)
}

const bound = myBind(announce, desk, '议程')
bound('补充') // 夜班主持 报 议程

真实 bind 还支持 new 调用绑定函数等边界;但日常调试时,把它想成“返回一个已经把 this 和前几项参数写死的函数”就够用了。看到第三方工具里的 fn.bind(ctx),就是在显式修复本章开头的 const fn = obj.method 问题。

DOM 与宿主对象:this 有时由平台指定

部分 DOM API 在调用监听器时,会把 this 设为触发事件的元素:

js
button.addEventListener('click', function () {
  console.log(this === button) // 通常为 true
})

这与“隐式绑定”类似,但规则来自宿主,而不是你写的对象字面量。换成箭头函数后,这条宿主规则不再适用,因为箭头函数根本不接收自己的 this

读 MDN 或框架文档时,看到“回调里的 this 指向某某”,要区分:是 JavaScript 的四种绑定之一,还是 DOM/Web API 的额外约定。

试一试:预测后再运行

把下面代码抄进控制台,先写预测顺序,再执行:

js
const shop = {
  name: '面馆',
  lines: [],
  add(line) {
    this.lines.push(line)
  },
}

const add = shop.add
add('加辣')
console.log(shop.lines.length)

shop.add('不要葱')
console.log(shop.lines.length)

第一行 add('加辣') 往往改不了 shop.lines,因为 this 不是 shop;第二行 shop.add 才是隐式绑定。把预测写进注释,再对照结果,比只看文章记得牢。

读错误堆栈时别忽略调用方式

TypeError: Cannot read properties of undefined 常出现在 this.foothis 不是预期对象时。堆栈顶是函数内部,但真正要看的往往是上一帧的调用表达式:是 obj.method() 还是 const f = obj.method; f()

养成习惯:从报错行往上找谁以什么形式调用了这个函数。这比死记“Vue 里 this 会丢”更能迁移到别的框架。

回调注册表里的 this:一次登记,多次调用

事件总线、Node 风格 EventEmitter、DOM 监听,本质都是“把函数放进一张表,将来由平台调用”。平台调用时往往不会帮你补 obj. 前缀,因此默认绑定与严格模式规则会原样生效。

写注册代码时,可以默认采用以下之一:

  • 箭头函数:() => obj.method()
  • 显式绑定:obj.method.bind(obj)
  • 包装函数:在类里用字段箭头属性

团队规范里固定一种,比每个人临场发挥更不容易在 code review 里漏掉 this 问题。

小结:三条问题线

读完本章,遇到 this 相关困惑,依次问:

  1. 调用表达式长什么样?有没有点号左侧的对象?
  2. 是不是箭头函数?若是,外层 this 在定义时是什么?
  3. 是不是被 call / apply / bind / new 改过规则?

能答这三问,大部分业务代码里的 this 异常都能定位。把本章与第二章对照阅读,效果最佳。

下一步:暂时轮不到执行的代码去了哪里

目前讨论的都是已经获得执行权的同步代码。可 setTimeout 回调、点击事件和网络结果显然不会一直留在调用栈上等待。

它们由谁计时、在哪里排队,又为什么“零毫秒”也不能插进当前函数中间?下一章从一个不太守时的定时器开始,进入事件循环与任务队列。读完那一章,再回头看本章的 setTimeoutthis 丢失案例,两条线会更容易串起来。