尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Vue中的箭头函数this:为什么会丢失,怎样避坑?

Vue中的箭头函数this:为什么会丢失,怎样避坑? “为什么我在 Vue 里一用箭头函数就报 this 找不到”这是我做前端这几年被问得最多的问题之一尤其是在面试和新人 code review 的时候。箭头函数写起来确实香少敲几个字母this 还不用管但放到 Vue 的选项式 API 里它就成了最容易翻车的隐形炸弹。网上讲箭头函数和 Vue 的文章很多大多是“这里不要用箭头函数那里不要用”的结论式罗列看完了还是不知道为什么换个场景照样踩坑。我打算用一篇文章把这件事彻底讲透先搞清楚箭头函数的 this 到底是什么机制再逐个拆解 Vue 选项式 API 里的禁区然后告诉你哪些地方箭头函数反而更好用最后把 Vue 3 组合式 API 里完全不同的使用规则也梳理清楚。这篇文章适合刚入门 Vue 的新手也适合写过一阵子但一直被 this 折磨的初中级前端看完能省下大量排查时间。1. 先搞懂箭头函数的 this 到底绑定在哪很多人用箭头函数出错根子上是没想明白一件事普通函数和箭头函数的 this 绑定规则是完全相反的两套逻辑。这不是 Vue 的问题是 JavaScript 语言本身的设计只不过 Vue 的选项式 API 把这个问题放大了。1.1 普通函数的 this谁调用就指向谁普通函数的 this 是在调用时确定的规则可以简化成一句话谁调用了这个函数函数内部的 this 就指向谁。直接调用fn()this 在非严格模式下是 window浏览器里严格模式下是 undefined通过对象调用obj.fn()this 指向 obj通过fn.call(ctx)调用this 就是我们手动指定的 ctx。Vue 选项式 API 之所以能让我们在 methods 里用 this 访问 data、computed 和其他方法就是因为它利用了普通函数的这个特性。Vue 在初始化组件实例的时候会遍历 methods 对象里的每一个方法把它们绑定到组件实例 vm 上绑定方式类似于vm.methodName bind(method, vm)。也就是说你写的methods: { handleClick() { this.count } }实际执行的时候Vue 是在拿组件实例去调用这个函数函数内部的 this 自然就指向组件实例了。这里的核心是普通函数允许调用方通过 bind/call/apply 改变 thisVue 才能把方法和实例捆绑起来。1.2 箭头函数的 this定义时就焊死了箭头函数的规则完全不同它没有自己的 this函数内部用到的 this 是在定义外层作用域时就确定下来的之后不管你怎么调用this 都不会变。用一个生活化的类比来说普通函数的 this 像是一个可以随时改签的航班箭头函数的 this 则像是出发前 24 小时已经值机锁死了座位起飞时间到了谁来了都没用。const obj { name: vue, normalFn: function() { console.log(this.name) }, arrowFn: () { console.log(this.name) } } obj.normalFn() // vuethis 指向 obj obj.arrowFn() // undefinedthis 指向定义时所在的外层作用域比如 window 或 undefined不管你是写obj.arrowFn()还是把箭头函数单独拿出来调它内部的 this 永远都是定义那一刻外层作用域的 this。箭头函数的 this 本质上是词法作用域的一部分和变量提升那套规则是同一套体系。1.3 连带影响的三个隐藏特性没有 arguments、不能 new、没有 prototype箭头函数除了 this 不同还有三个连带特性这三兄弟也经常会成为 Vue 项目里的隐性问题没有自己的 arguments 对象箭头函数里的 arguments 会沿作用域链向上找如果外层也没有就直接报错。在 Vue 的 methods 里想通过 arguments 拿到参数列表用箭头函数就废了。不能作为构造函数不能用new调用因为箭头函数没有 [[Construct]] 内部方法。Vue 组件选项本身不是靠 new 构造组件的但如果你在某些工厂函数里试图用箭头函数去构造对象会直接抛 not a constructor。没有 prototype 属性这个特性在 Vue 中影响相对小但做插件开发或者在 mixin 里给函数做原型扩展时会莫名发现问题。记住这三条再看 Vue 官方文档里“不要在选项属性或回调上使用箭头函数”的警告就不会觉得突兀了。官方那句话的全貌是“箭头函数没有 thisthis 会作为变量一直向上级词法作用域查找直到找到为止……这会导致诸如 this.myMethod() 或 this.$emit() 之类的代码无法正常工作。”现在你应该能完全读懂这句话了。2. Vue 选项式 API 里的坑这些位置别用箭头函数搞清楚了箭头函数 this 的机制接下来我们逐个看 Vue 选项式 API 里的重灾区。这些位置排列组合起来就是无数个“为什么我的 Vue 页面一刷新就报错”的经典现场。2.1 data 必须是一个返回对象的普通函数先看 data。Vue 2 和 Vue 3 的组件定义里data 都必须是一个函数返回一个新的对象。很多人知道“要写成函数”但没意识到这里也有箭头函数的坑// 错误示范 export default { data: () ({ count: 0, name: this.defaultName // 这里的 this 是什么模块作用域或 undefined }) }这个 this 不可能是组件实例因为 data 函数被 Vue 调用时Vue 希望它内部的 this 指向组件实例这样才能在 data 里通过 this 访问 props、$router、$store 等实例属性。但箭头函数在定义时已经把 this 锁在了组件文件的最外层作用域里Vue 的绑定机制完全失效。还有一个更隐蔽的问题箭头函数不能作为构造函数而 data 本质上是一个“每次进到组件就 new 一个数据对象”的工厂。虽然用箭头函数返回对象字面量不至于像new那样直接抛错但它已经违背了 Vue 对 data 的设计意图单例共享的隐患。如果 data 返回的对象恰好被某种机制复用或者你在模块顶层定义了一个对象然后在箭头函数里返回它多个组件实例之间就会共享同一份数据改一个全变。// 正确的常规写法 export default { data() { return { count: 0, name: this.$route.query.name || } } }这里给一个经验如果我在做 Vue 2 项目且组件里 data 需要访问实例属性比如 this.$store 或 this.$route我习惯把需要的值在 data 返回之前先用普通变量接一下再放到返回对象里。这不算强制要求但能减少很多“为什么 data 里拿不到 store”的困惑。2.2 methods 里的箭头函数经典翻车现场methods 是我见过的箭头函数重灾区第一名。原因很好理解method 看着像“方法”写着写着就顺手写了箭头函数。症状也很统一——this 变成 undefined然后页面白屏或控制台红色报错。// 错误示范 export default { data() { return { count: 0 } }, methods: { increment: () { this.count // TypeError: Cannot read properties of undefined (reading count) }, getTotal() { // 反过来在普通函数内部又嵌套一个箭头函数时反而没问题 return this.count * 10 } } }报错的原因很直接increment 是用箭头函数写的Vue 想要通过bind(method, vm)给它绑定 this但箭头函数没有自己的 thisbind 根本不起作用。于是 increment 内部的 this 沿词法作用域往组件模块的外层找找到的往往是 undefined现代前端构建工具默认开启严格模式模块顶层的 this 是 undefined。这里我强烈建议理解一下“Vue 在编译或者初始化时到底做了什么”。Vue 2 在 initMethods 里会遍历 methods对每个方法执行vm[key] bind(methods[key], vm)。Vue 3 也类似最终用到的都是普通函数的可绑定特性。也就是说methods 的正确写法只有一种普通方法。// 正确示范 export default { data() { return { count: 0 } }, methods: { increment() { this.count } } }值得多说一句的是methods 里经常需要互相调用比如 handleClick 内部调用 fetchList。如果 handleClick 用普通函数fetchList 也用普通函数那么methods: { async fetchList() { const res await api.getList() this.list res.data }, handleClick() { this.fetchList() // this 指向组件实例没问题 } }2.3 生命周期钩子与 computed同样踩雷生命周期钩子也是箭头函数的重灾区。created、mounted、beforeUnmount 这些钩子Vue 在调用时同样会绑定组件实例作为 this。如果写成箭头函数export default { data() { return { list: [] } }, mounted: () { this.loadData() // this 不是组件实例这里直接挂 }, methods: { loadData() { // ... } } }这个错误和 methods 是同一个道理症状却更隐蔽因为生命周期钩子里如果不直接调用方法只是 console.log 一个值可能不会立刻报错等你发现数据没加载出来已经绕了好几圈。computed 和 watch 同样要小心。computed 的 getter 和 setter 内部通常都需要访问组件实例上的数据和方法computed: { // 错误 fullName: () this.firstName this.lastName, // 正确 fullName() { return this.firstName this.lastName }, // 带 setter 的写法 reversedMessage: { get() { return this.message.split().reverse().join() }, set(value) { this.message value } } }watch 的 handler 如果依赖 this同样不能用箭头函数watch: { // 如果 handler 里要访问组件实例箭头函数就废了 query(newVal) { this.debouncedSearch() }, // 不需要 this 的场景箭头函数能跑但为了统一建议还是普通函数 currentPage(newVal, oldVal) { // 纯逻辑不碰 this } }2.4 选项式 API 各选项 this 绑定对照表我把这些经验整理成一张速查表方便遇到问题的时候直接对号入座选项位置推荐写法this 的预期指向写成箭头函数的结果data普通函数组件实例undefined访问实例属性直接报错methods普通函数组件实例undefined调用实例方法/数据直接报错computed getter/setter普通函数组件实例undefined计算属性结果异常watch handler普通函数组件实例undefined拿不到组件数据和方法created/mounted 等生命周期普通函数组件实例undefined初始化逻辑静默失效filtersVue 2普通函数组件实例undefined过滤器内 this 丢失这张表可以收藏笔试或者面试前看一遍基本能覆盖 90% 的“Vue 中箭头函数 this 丢失”问题。3. 箭头函数在 Vue 里的正确用法从回调到高阶函数看到这里别觉得箭头函数在 Vue 里就只能原地去世了。恰恰相反在正确的场景里箭头函数是 Vue 开发者最顺手的工具。它的核心价值在于需要捕获外层 this 的时候普通函数做不到箭头函数是唯一解。3.1 定时器、事件与 Promise 回调中的 this 保存Vue methods 里的方法我们要求用普通函数是为了让 this 指向组件实例。但一旦进入异步回调比如 setTimeout、setInterval、addEventListener、Promise.then情况就反过来了普通函数的 this 会丢失箭头函数反而能稳稳接住。export default { data() { return { count: 0, timer: null } }, methods: { startCountdown() { // 错误普通函数回调中的 this 不再指向组件实例 this.timer setInterval(function() { this.count-- // 这里 this 是 window / undefinedcount 减不动 }, 1000) // 正确箭头函数捕获了定义位置的 this也就是组件实例 this.timer setInterval(() { this.count-- }, 1000) }, async loadData() { // 正确then 回调用箭头函数this 保持组件实例 const res await api.getData().then(response { this.loading false return response.data }) } }, beforeUnmount() { clearInterval(this.timer) } }这套思路在事件监听里也适用mounted() { window.addEventListener(resize, this.handleResize) }, methods: { handleResize() { this.width window.innerWidth // 没问题Vue 绑定的是这个函数本身 } }但如果是在 init 过程中给普通 DOM 添加监听写成回调函数就要小心。比如mounted() { document.getElementById(box).addEventListener(click, function() { this.visible true // 这里的 this 指向 DOM 元素不是组件实例 }) document.getElementById(box).addEventListener(click, () { this.visible true // 箭头函数捕获外层 this指向组件实例正确 }) }我个人的习惯是凡是在 methods 内部需要往定时器、事件、Promise 等异步回调中传递函数时一律用箭头函数。这也是为什么我在公司代码规范里会写一条methods 本身用普通函数methods 内部的回调用箭头函数。3.2 data/methods 内部结合箭头函数的高阶姿势数组的高阶函数就是 map、filter、reduce、forEach 这些它们接收的回调在执行时默认会修改 this 的指向指向 undefined 或全局对象如果在回调里想访问组件实例属性箭头函数是唯一省心的方案。methods: { calculateTotal() { // 错误普通函数回调用不了外层的 this.cartItems return this.cartItems.reduce(function(sum, item) { return sum item.price * item.quantity }, 0) // 正确箭头函数捕获外层 this return this.cartItems.reduce((sum, item) { return sum item.price * item.quantity }, 0) }, formatItems() { return this.items.map(item ({ ...item, label: this.formatLabel(item) // 这里的 this 指向组件实例 })) } }还有一种场景防抖节流。比如搜索框输入后 300ms 触发接口请求很多人用 lodash 的 debouncemethods: { // 错误debounce 内部回调执行时this 已经丢失 onSearch: debounce(function() { this.fetchResults() // this 是 undefined }, 300), // 正确用普通方法 箭头函数包装 // ... }这里踩坑要区分两种情况如果 debounce 包在普通方法内部使用里面的回调写箭头函数没问题如果直接把 debounce 包装后的函数放到 methods 上等触发时 this 就丢了因为 debounce 内部最终是用自己的定时器调用目标函数调用场景已经不是组件实例了。遇到这种场景我的解法是把 debounced 函数创建在 created 里并用箭头函数保住 thiscreated() { this.debouncedSearch debounce((keyword) { this.search(keyword) // 这里的 this 指向组件实例 }, 300) }, methods: { search(keyword) { /* ... */ } }3.3 模板事件处理能用箭头函数吗模板里的事件处理clickhandleClick这里能不能直接写箭头函数表达式比如button click() handleClick()。技术上 Vue 支持这种写法但我不建议原因有两条模板里写内联箭头函数会让模板变乱调试的时候很难断点。如果箭头函数内部需要 this范围就很微妙普通用户很难判断此时的 this 是什么。Vue 模板编译会在渲染上下文中调用函数这个上下文里 this 指向组件实例代理但你写一个箭头函数包了一层里面的 this 就不一定是你想的那个了。简单说模板事件处理器最稳妥的写法还是clickhandleClickmethods 里写普通方法。如果非要传递参数可以clickhandleClick(item.id)这也是普通方法表达式。3.4 Vuex / Pinia / Router 中的边界情况很多项目是 Vue Vuex Vue Router箭头函数在这里也有边界。先看 Vuex。Vuex 3 的 mutation/action 里this 指向 store 实例所以如果你在 mutation 里写成箭头函数想通过 this 访问 store 的其他属性就会失败。不过 Vuex 更常见的写法是从参数里解构 commit、state// 有 this 依赖不能用箭头函数 mutations: { setUserInfo: (state, payload) { // 想通过 this 访问其他 mutation对不起this 不是 store }, setUserInfo(state, payload) { state.userInfo payload } } // 没有 this 依赖箭头函数能跑但为了风格一致还是建议普通函数 actions: { fetchUser: ({ commit }) { return api.getUser().then(res commit(setUserInfo, res.data)) } }Vue Router 的导航守卫比如router.beforeEach((to, from, next) {})这类回调不涉及组件实例 this用箭头函数完全没问题。但组件内守卫里有讲究// beforeRouteEnter 本来就不让用 this箭头函数无所谓 beforeRouteEnter: (to, from, next) { next(vm { // 这里的 vm 才是组件实例 }) }, // beforeRouteUpdate 在这里是有组件实例 this 的但箭头函数拿不到 // 所以要用普通函数 beforeRouteUpdate(to, from) { this.loadData(to.params.id) }Pinia 的情况和 Vuex 类似。Options 风格的 actions 里this 指向 store 实例建议用普通函数Setup 风格定义 store 时因为你完全面向 ref/reactive 编程箭头函数随便用因为在 setup 里你根本不需要 this。4. Vue 3 组合式 API 完全不同箭头函数可以放心用Vue 3 推出组合式 API 之后this 问题被大大稀释了。原因很简单组合式 API 的设计目标就是让你不再依赖 this而是通过 ref、reactive、computed 这些显式的变量来组织状态和逻辑。这直接改变了箭头函数的使用规则。4.1 setup 的 this 本来就是 undefined先说一个让很多人意外的点Vue 3 的 setup 函数里this 不是组件实例而是 undefined。这是官方设计setup 的执行时机在组件实例创建之前this 尚未绑定所以官方文档明确说“在 setup 中你应该避免使用 this因为它不会指向组件实例”。export default { setup() { // 这里的 this 是 undefined console.log(this) // undefined const count ref(0) const increment () { count.value // 完全不依赖 this箭头函数毫无问题 } return { count, increment } } }既然 setup 里本来就没有 this那在 setup 内部定义方法时用箭头函数就不会有“this 丢失”的问题。恰恰相反因为组合式函数往往要把一组逻辑包在一起箭头函数的简洁写法反而更顺手。在script setup语法糖里也是一样的逻辑script setup import { ref, computed } from vue const count ref(0) const double computed(() count.value * 2) // 这里写成箭头函数或普通函数都行推荐箭头函数作为风格 const increment () { count.value } /script template button clickincrementcount is {{ count }}/button /template这里我建议团队内统一风格在 script setup 顶层和 setup 函数内部一律用箭头函数声明普通逻辑函数。因为这里不涉及 this箭头函数短还能避免误用 this。4.2 组合式函数开发里的箭头函数实践组合式函数Composables就是把状态和相关行为抽成一个函数// useCounter.js import { ref, computed, onMounted } from vue export function useCounter() { const count ref(0) const isEven computed(() count.value % 2 0) function increment() { count.value } function decrement() { count.value-- } onMounted(() { console.log(counter mounted) }) return { count, isEven, increment, decrement } }在这里increment 和 decrement 写普通函数或箭头函数没有任何区别因为它们内部用的是闭包变量 ref 的 value不涉及 this。我写了多个中大型 Vue 3 项目之后得出的组合式函数内部风格建议是watchEffect、computed、onMounted 等回调一律箭头函数导出给模板用的方法保持普通函数或箭头函数都行团队定一个规则即可。4.3 watchEffect / watch 回调与箭头函数watchEffect 和 watch 的默认回调官方推荐就用箭头函数因为设计上它们压根不依赖 thisconst count ref(0) const user reactive({ name: vue }) watchEffect(() { console.log(count.value) }) watch(count, (newVal, oldVal) { console.log(newVal, oldVal) }) watch( () user.name, (newVal, oldVal) { // 箭头函数在这里没有任何问题 } )有人会问那如果 watch 的回调里需要访问组件方法呢在script setup里组件方法本身就是通过 import 或函数声明定义的变量直接闭包引用即可完全不需要 thisscript setup import { watch, ref } from vue const query ref() function search() { // 发起接口请求 } watch(query, (newVal) { // 直接调用 search不需要 this.search search() }) /script这其实就是组合式 API 最大的收益之一把所有依赖关系变成显式的词法闭包你不再需要猜 this 是谁。4.4 render / JSX 中的写法Vue 3 里用 render 函数或 JSX 写组件时箭头函数也很常见。如果你用的是组合式 API render 函数import { h, ref } from vue export default { setup() { const count ref(0) return () h(button, { onClick: () count.value }, count is ${count.value}) } }render 函数内部的事件回调写成箭头函数非常自然因为闭包直接捕获了 setup 作用域里的 count。如果使用选项式 API 的 render 方法那就要注意 this 了因为 render 方法内部 this 是组件实例代理export default { data() { return { count: 0 } }, render() { // 这里必须用普通函数因为需要 this.count return h(button, { onClick: () this.count // 事件回调里可以用箭头函数因为需要捕获 render 的 this }, count is ${this.count}) } }这种混合写法在迁移老项目时经常遇到我的建议是要分清楚“哪一层需要 this”这一层需要 this就用普通方法这一层只是回调需要继承外层 this就用箭头函数。这套逻辑能帮你应对几乎所有 Vue 3 的 this 问题。5. 实战排查this 丢失问题定位与修复前面讲了原理和场景最后分享一些我实际排查 Vue 项目 this 问题时积累的经验。如果你在代码里看到类似 “Cannot read properties of undefined (reading xxx)” 的报错十有八九就是箭头函数把 this 弄丢了。5.1 五个高频翻车场景与报错信息我把这些年见到的真实报错场景整理一下你可以对照排查场景典型写法报错/现象修复方案methods 里写了箭头函数handleClick: () { this.load() }Cannot read properties of undefined (reading load)改为普通方法data 里访问 thisdata: () ({ name: this.$route.query.name })this是 undefinedname 取不到data 改为普通函数生命周期钩子箭头函数created: () { this.init() }初始化逻辑根本不执行页面数据空白created 改普通函数定时器回调里用普通函数setInterval(function(){ this.count-- }, 1000)count 不变化或者报 count 找不到回调改箭头函数事件监听回调普通函数el.addEventListener(click, function(){ this.open() })一直报 open 不是函数回调改箭头函数或把 this 存到变量这五个场景是我在带新人和做 code review 时最常看到的基本覆盖了 80% 的 this 相关问题。5.2 排查思路先问 this 从哪来遇到 this 异常我一般不会先去翻代码而是先问三个问题这个函数是普通函数还是箭头函数这个函数定义位置的外层是否有 this外层的 this 是什么这个函数被调用时调用方式是什么如果是普通函数this 由调用方式决定赶紧去看谁调用了它如果是箭头函数this 由定义位置决定赶紧去看它定义在外层的哪个作用域。把思路捋顺了定位问题一般不会超过五分钟。还有一个技巧在可疑代码里临时加一行console.log(this)先确认 this 到底是 undefined、window 还是组件实例。然后根据结果反推是否用了箭头函数。工具层面上Vue DevTools 里能看到组件实例但我建议先学会在代码里判断这是基本功。5.3 场景速查表普通函数还是箭头函数把全文的核心规则浓缩成一张决策表什么时候用普通函数什么时候用箭头函数场景建议定义 Vue 选项式 API 的 data/methods/computed/lifecycle/watch handler普通函数Vue 2 filters 里需要访问 this普通函数methods 内部使用 setTimeout/setInterval/Promise/事件监听回调箭头函数methods 内部使用 map/filter/reduce/forEach 且回调需要访问 this箭头函数模板事件处理器clickxxxmethods 中普通函数方法组合式 API setup / script setup 内定义逻辑函数推荐箭头函数watchEffect / watch 回调用到的是闭包变量箭头函数render 函数选项式外层普通方法内部回调用箭头函数Vuex/Pinia 的 actionsoptions 风格需要 this普通函数Vuex/Pinia 的 actions 用参数解构setup 风格箭头函数/普通函数均可统一即可这张表就是我对 Vue 箭头函数使用规则的全部心得。你把它保存在项目文档里新同事接手代码时直接发过去能省很多沟通成本。5.4 我现在的团队约定最后分享一个我自己的项目管理小实践。为了避免大家在 this 上反复踩坑我后来在代码规范里加了几条硬性约定选项式 API 组件的 data、methods、computed、watch handler、生命周期钩子一律用普通函数/方法不用箭头函数。methods 内部嵌套的回调如 setInterval、事件监听、Promise then、数组高阶函数回调一律用箭头函数。组合式 APIsetup / script setup中不用 this所有共享状态通过 ref/reactive 显式返回函数定义尽量用箭头函数。代码 review 模板里有一条专门的检查项搜索“ {”出现在 options 根级的位置必须说明理由。这些约定执行了一年多团队里因为 this 问题提的 bug 基本绝迹了。其实箭头函数本身没有好坏之分它只是两种 this 规则的其中一种真正的坑是我们把规则用错了场景。理解了机制再按照场景去套用你会发现 Vue 里箭头函数的使用规则一点都不复杂甚至可以说相当统一选项式 API 的根级选项要 this用普通函数嵌套回调要继承 this用箭头函数组合式 API 根本不依赖 this随手写都行。我的个人体会是遇到 Vue 的 this 问题与其死记硬背“这里不能用箭头函数”不如先停下来问一句这里的 this 应该是谁答案是组件实例就选普通函数答案是不需要 this 或者需要引用外层 this箭头函数就是最好的选择。把这个习惯带到日常开发里你不但能在 Vue 里用好箭头函数换个框架、换个语言这套辩证的思考方式也一样用得上。
返回列表