本文目录
会议室里同一支话筒,谁被点名谁发言。话筒没有固定主人;“这次谁在说”,取决于主持人把话筒递给了谁,而不是话筒上贴了谁的名字。
this 也类似:同一个函数体,每次调用时“当前这次执行代表谁”,不由函数写在哪个文件里决定,而主要由调用方式决定。第二章讲的是名字沿词法环境向外找;这一章讲的是“这次调用里的 this 绑给了谁”。
第二章区分过闭包与 this,这里把规则摊开
闭包回答:内层函数还能读到哪些词法绑定。this 回答:这次函数执行时,当前对象上下文是谁。两者都挂在执行上下文上,但查找规则完全不同。
别急着背“箭头函数没有 this”这种口诀。先记住分工:变量名走词法链;this 走绑定规则(箭头函数例外,它从定义处捕获外层的 this)。
先猜输出:同一个函数,三次调用三种 this
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。
function report() {
'use strict'
console.log(this)
}
report() // undefined隐式绑定:obj.method() 形式,this 指向点号左侧对象。
const cart = {
items: 2,
total() {
return this.items * 10
},
}
cart.total() // 20但若你把 total 赋给变量再调,隐式绑定会丢失,回到默认绑定。
显式绑定:call、apply、bind 强行指定 this。
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 的完整关系在第十二章展开)。
function Ticket(id) {
this.id = id
}
const t = new Ticket(7)
console.log(t.id) // 7判断一次调用时,可以按优先级想:是否 new?是否 call/apply/bind?是否 obj.fn()?否则多半是默认绑定。
箭头函数:捕获词法 this,而不是每次重新绑
const timer = {
seconds: 0,
start() {
setInterval(() => {
this.seconds += 1
console.log(this.seconds)
}, 1000)
},
}箭头函数没有自己的 this 参数,它会捕获定义时外层词法环境的 this。因此回调里仍指向 timer。
若改成普通函数:
setInterval(function () {
this.seconds += 1 // 非严格模式下 this 往往是 window
}, 1000)this 会在每次回调调用时重新按默认规则绑定,通常不再是 timer。
类字段箭头属性也常用于“把方法当回调传递仍保留实例”:
class Panel {
label = '订单'
render = () => {
console.log(this.label)
}
}这是语法层面的便利,本质仍是词法捕获。不要把它和普通原型方法混为一谈。
class、事件监听与容易丢 this 的写法
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 事件里同样常见:
button.addEventListener('click', desk.speak) // 可能丢 this
button.addEventListener('click', () => desk.speak()) // 通过 desk 显式调用,保留 desk
button.addEventListener('click', desk.speak.bind(desk)) // 显式 bind读框架源码或写组件时,看到方法被解构、被当作回调传递,先问:这次调用还是 obj.method() 吗?
严格模式、模块与全局 this 的收缩
function show() {
'use strict'
console.log(this)
}
show() // undefined,不再是 windowES 模块顶层默认严格模式,顶层 this 通常是 undefined,而不是 window。这会影响你把普通函数当回调直接传递时的默认绑定。
在浏览器里,全局对象可通过 globalThis 访问。不要把“非严格默认绑定到 window”当成可靠特性写进库代码;显式传参或 bind 更清晰。
借方法调用:数组、类数组与函数借用
const data = { 0: 'a', 1: 'b', length: 2 }
Array.prototype.push.call(data, 'c')
console.log(data.length) // 3push 本是为数组写的,但通过 call 把 this 指到类数组 data 上,就能复用同一套实现。这再次说明:函数体与 this 指向的对象可以分离。
Function.prototype.call / apply 也是可借用的:
function tag(line) {
return `[${this.prefix}] ${line}`
}
tag.call({ prefix: '会务' }, '开始了')读源码时看到 xxx.call(yyy),先找:这次要把 this 临时指给谁?
Vue 选项式 API 里常见的 this 丢失
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 困惑,按这个顺序问:
- 是否
new调用? - 是否通过
call/apply/bind创建或调用? - 是否
obj.method()形式? - 是否箭头函数(若是,去看定义处外层
this)? - 否则按默认绑定,并确认是否在严格模式或模块中。
把这个流程写成便签贴在显示器旁,比背十张表格更快。调试时配合 console.log(this) 与调用栈顶部的调用表达式,一般两轮就能定位。
容易踩的坑:别把 this 当成闭包里的变量
- “函数在哪定义,
this就在哪”——那是箭头函数的行为,不是普通函数的规则。 - “
this像第一个参数那样由调用者自动传入”——只有call/apply/bind才会显式指定;普通调用不会。 - 把
this.xxx和闭包捕获的xxx混用——词法变量来自环境链,this来自当前调用的绑定。类里this.count是实例属性;外层const count与它无关,除非你主动赋值。 - 以为
bind之后还能被call改掉——绑定函数通常已固定this(用new调用绑定函数是另议边界)。 - 以为
obj?.method()会改变绑定规则——可选链只短路访问;一旦真的执行method(),隐式绑定仍指向obj。 - 在
class字段里写普通函数属性,却期待它与原型方法一样共享:
class Bad {
handler = function () {
console.log(this)
}
}每个实例都会得到自己的 handler 函数对象,内存与 this 行为都与原型方法不同。这是刻意的实例字段,不是“写错了”。弄清要共享行为(放 prototype / class 方法体)还是每实例一份(字段初始化器),再选写法。
Promise 与 async 不会自动绑定 this
const repo = {
items: [1, 2, 3],
async load() {
await Promise.resolve()
console.log(this.items)
},
}
const fn = repo.load
fn() // 默认绑定,this 不是 repoawait 只暂停当前 async 函数,不会替你修复 this。若把 repo.load 交给路由钩子或事件总线,仍要 bind 或箭头包装。第十章讨论的是执行权何时恢复;恢复后 this 仍按本章规则来。
与闭包对照:同一回调里的两个维度
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,与外层 render 的 this 无关。箭头回调则捕获 render 执行时的 this。读这类代码要同时看:词法上能读哪些变量(闭包)与 这次函数体的 this 是谁。
第一章说执行上下文同时包含词法环境与 this 绑定;第二章与第三章只是把两块拼图拆开。面试若问“箭头函数为什么没有 arguments / this”,答案都应回到“它复用外层的词法环境”。
手写 bind 的极简版:看清固定 this 的本质
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 设为触发事件的元素:
button.addEventListener('click', function () {
console.log(this === button) // 通常为 true
})这与“隐式绑定”类似,但规则来自宿主,而不是你写的对象字面量。换成箭头函数后,这条宿主规则不再适用,因为箭头函数根本不接收自己的 this。
读 MDN 或框架文档时,看到“回调里的 this 指向某某”,要区分:是 JavaScript 的四种绑定之一,还是 DOM/Web API 的额外约定。
试一试:预测后再运行
把下面代码抄进控制台,先写预测顺序,再执行:
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.foo 而 this 不是预期对象时。堆栈顶是函数内部,但真正要看的往往是上一帧的调用表达式:是 obj.method() 还是 const f = obj.method; f()?
养成习惯:从报错行往上找谁以什么形式调用了这个函数。这比死记“Vue 里 this 会丢”更能迁移到别的框架。
回调注册表里的 this:一次登记,多次调用
事件总线、Node 风格 EventEmitter、DOM 监听,本质都是“把函数放进一张表,将来由平台调用”。平台调用时往往不会帮你补 obj. 前缀,因此默认绑定与严格模式规则会原样生效。
写注册代码时,可以默认采用以下之一:
- 箭头函数:
() => obj.method() - 显式绑定:
obj.method.bind(obj) - 包装函数:在类里用字段箭头属性
团队规范里固定一种,比每个人临场发挥更不容易在 code review 里漏掉 this 问题。
小结:三条问题线
读完本章,遇到 this 相关困惑,依次问:
- 调用表达式长什么样?有没有点号左侧的对象?
- 是不是箭头函数?若是,外层
this在定义时是什么? - 是不是被
call/apply/bind/new改过规则?
能答这三问,大部分业务代码里的 this 异常都能定位。把本章与第二章对照阅读,效果最佳。
下一步:暂时轮不到执行的代码去了哪里
目前讨论的都是已经获得执行权的同步代码。可 setTimeout 回调、点击事件和网络结果显然不会一直留在调用栈上等待。
它们由谁计时、在哪里排队,又为什么“零毫秒”也不能插进当前函数中间?下一章从一个不太守时的定时器开始,进入事件循环与任务队列。读完那一章,再回头看本章的 setTimeout 与 this 丢失案例,两条线会更容易串起来。