
先還原一個很多 Vue 新手都經(jīng)歷過的“案發(fā)現(xiàn)場”你在 methods 里寫了一個loadData用來請求接口數(shù)據(jù)請求成功后想在回調(diào)里給this.list賦值。結(jié)果控制臺直接甩給你一句Cannot read properties of undefined (reading list)。你第一反應(yīng)是“接口沒返回數(shù)據(jù)”回頭查了半天最后發(fā)現(xiàn)報錯的是this——在回調(diào)函數(shù)里它根本不是你的 Vue 組件實例。這個問題的本質(zhì)不是“代碼寫錯了”而是 JavaScript 里一個極其迷惑的機(jī)制在搗鬼回調(diào)函數(shù)中的 this 不會自動指向當(dāng)前 Vue 組件實例。今天咱們就用最直白的語言把這個“失蹤案”從頭到尾捋清楚看完你不僅能排雷還能順手把面試官最愛問的 this 指向問題拿下。1. Vue 的 this 到底是什么搞清楚組件實例和調(diào)用者1.1 組件實例this 背后那個會裝數(shù)據(jù)的“工具箱”在 Vue 組件里寫this.message、this.loadData()本質(zhì)上是在通過一個“總工具箱”訪問東西。這個工具箱就是組件實例Component Instance。Vue 在組件初始化的時候會把data里的數(shù)據(jù)、methods里的方法、computed里的計算屬性全部掛到同一個實例對象上并且通過代理機(jī)制讓你能用this.xxx直接訪問??梢赃@樣類比組件實例就是一個移動工具箱this.message就是從工具箱里抽出“消息”這個工具。工具箱里裝著數(shù)據(jù)、方法、生命周期鉤子是你在組件內(nèi)部干活的唯一入口。但要注意這個“工具箱”不是你寫代碼時隨便喊一嗓子就出現(xiàn)的。它是在組件被創(chuàng)建、初始化、掛載這一整套流程中被“組裝”好的。你平時在methods里寫的函數(shù)Vue 會綁定到這個實例上所以你在this.loadData()里訪問this.message是正常的。1.2 普通函數(shù) this 由調(diào)用者決定一個傳話人的比喻JavaScript 普通函數(shù)的this不是定義時確定的而是在函數(shù)真正被調(diào)用的那一刻由“調(diào)用者”決定的。這是個很多新手顛覆認(rèn)知的點(diǎn)你以為函數(shù)寫在組件里它的this就一定指向組件錯了。用一個傳話人的比喻解釋你雇了一個傳話人函數(shù)讓他拿著你的話去公司傳達(dá)。如果你親自帶他進(jìn)公司說“你去公司行政部傳個話”那他知道自己在公司面對的行政部但如果你讓他自己去大街上喊一嗓子他就不知道自己該找誰了。放到代碼里就是兩種常見情況const obj { name: Vue, say() { console.log(this.name) } } obj.say() // 調(diào)用者是 objthis 指向 obj輸出 Vue const fn obj.say fn() // 調(diào)用者變成了全局環(huán)境this 不再指向 obj你發(fā)現(xiàn)沒有函數(shù)本身沒有變變的是“誰去調(diào)它”。obj.say()這種帶點(diǎn)語法的調(diào)用調(diào)用者是obj把函數(shù)取出來變成fn()這種裸調(diào)用調(diào)用者就成了全局環(huán)境瀏覽器里是window嚴(yán)格模式下是undefined。1.3 嚴(yán)格模式下的 undefined現(xiàn)代前端項目的默認(rèn)狀態(tài)你可能會問那丟失之后this到底是什么在瀏覽器環(huán)境非嚴(yán)格模式下裸調(diào)用函數(shù)的this指向window但在 ES6 模塊、Vue 工程化項目里代碼默認(rèn)處于嚴(yán)格模式裸調(diào)用函數(shù)的this就是undefined。這解釋了一個現(xiàn)象你訪問this.list報的是Cannot read properties of undefined說明this本身已經(jīng)是undefined了。而不是window.list單純拿不到值。所以這個“失蹤案”的完整鏈條是普通函數(shù)在回調(diào)里被某個內(nèi)部機(jī)制調(diào)用調(diào)用者不是組件實例加上嚴(yán)格模式導(dǎo)致 this 變成 undefined再去訪問屬性自然就崩了。2. 四類典型案發(fā)現(xiàn)場this 到底在哪里丟的2.1 setTimeout 計時器回調(diào)最常見的丟失現(xiàn)場先看一段真實到不能再真實的代碼export default { data() { return { message: Hello Vue } }, methods: { test() { setTimeout(function () { console.log(this.message) // TypeError: Cannot read properties of undefined }, 1000) } } }這段代碼為什么會崩setTimeout接受你傳入的函數(shù)然后在一個計時器回調(diào)機(jī)制中直接調(diào)用它。這個函數(shù)已經(jīng)不是由組件實例調(diào)用了而是由setTimeout的內(nèi)部調(diào)度邏輯調(diào)用?;卣{(diào)函數(shù)是普通函數(shù)this在嚴(yán)格模式下就是undefined訪問this.message必定報錯。實際項目里最常見的場景是進(jìn)頁面后用setTimeout延遲搜索、用setInterval輪詢接口、用定時器做防抖/節(jié)流。只要定時器回調(diào)里寫了普通函數(shù)this 直接失蹤。輪詢?nèi)绻€涉及賦值this.list ...那更慘整個頁面數(shù)據(jù)直接癱瘓。2.2 forEach/map 數(shù)組遍歷沒寫箭頭函數(shù)就會翻車數(shù)組遍歷的forEach、map、filter回調(diào)同樣危險。很多人覺得forEach是同步的應(yīng)該沒問題但同步不代表 this 會被傳遞methods: { updateList() { this.items.forEach(function (item) { this.checked true // this 不是組件實例 }) } }forEach內(nèi)部的fn(item, index, array)調(diào)用方式是無點(diǎn)語法調(diào)用在嚴(yán)格模式下this為undefined。這里有個冷知識forEach其實是接受第二個參數(shù)的arr.forEach(fn, thisArg)官方允許你手動傳入thisArg作為回調(diào)里的 this 指向。但是這個用法在團(tuán)隊協(xié)作中幾乎沒人用稍不留神就被人“優(yōu)化”掉。所以最穩(wěn)妥的還是箭頭函數(shù)后面會細(xì)講。2.3 axios / Promise 異步請求接口成功后拿不到 this這是業(yè)務(wù)代碼里最高頻的丟 this 現(xiàn)場。用 axios 或 fetch 請求接口在.then回調(diào)里更新頁面數(shù)據(jù)methods: { loadData() { axios.get(/api/list).then(function (res) { this.list res.data // this 不是組件實例報錯 }) } }Promise 的.then、.catch回調(diào)不能繼承外層函數(shù)的this。then里的回調(diào)由 Promise 調(diào)度器調(diào)用調(diào)用者是 Promise 內(nèi)部機(jī)制不是組件實例。還有一個更隱蔽的版本在async/await下面如果你寫成await axios.get(...)再去操作this.list其實沒問題因為await展開后代碼還停留在當(dāng)前函數(shù)作用域里this還是當(dāng)前函數(shù)的this。但如果你把.then當(dāng)作回調(diào)寫this 就丟了。所以你在項目里會看到很多“能跑”的老代碼都是因為在.then里用了箭頭函數(shù)。2.4 事件監(jiān)聽與父子組件傳參this 被傳遞時就變了原生事件監(jiān)聽也是丟 this 的重災(zāi)區(qū)。比如你在mounted里給window加滾動監(jiān)聽mounted() { window.addEventListener(scroll, this.handleScroll) // 回調(diào)執(zhí)行時 this 丟失 }, methods: { handleScroll() { console.log(this) // 不一定指向組件實例 } }window.addEventListener接收的是函數(shù)引用事件觸發(fā)時由事件系統(tǒng)調(diào)用調(diào)用者不是組件實例。需要注意的是Vue 模板里的clickhandleScroll不會丟 this因為 Vue 在編譯模板和綁定事件時內(nèi)部偷偷做了 bind 操作把 this 固定到組件實例上。但原生事件、第三方 UI 庫的回調(diào)、別人封裝的工具函數(shù)都沒有這個待遇。父子組件之間傳函數(shù)也有坑。父組件把this.parentMethod通過 props 傳給子組件子組件內(nèi)部可能在某個異步回調(diào)里調(diào)用它。只要這個函數(shù)在傳遞過程中被“拆”下來變成裸調(diào)用this 就丟了。這種問題特別難排查因為報錯位置在子組件內(nèi)部但根因在父組件傳參方式。3. 追根溯源為什么回調(diào)函數(shù)不繼承 this3.1 普通函數(shù) vs 箭頭函數(shù)決定 this 的兩條規(guī)則JavaScript 里的函數(shù)分兩大類this 規(guī)則完全相反函數(shù)類型this 決定方式特點(diǎn)普通函數(shù)調(diào)用時由調(diào)用者決定動態(tài)綁定點(diǎn)語法、裸調(diào)用、bind 都會影響箭頭函數(shù)定義時由外層詞法作用域決定沒有自己的 this向上找外層作用域箭頭函數(shù)沒有自己的this它的this是在定義的時候從外層詞法作用域繼承來的。也就是說箭頭函數(shù)內(nèi)部用到this就相當(dāng)于在外層作用域里找this。比如你在組件的methods里定義了一個箭頭函數(shù)這個箭頭函數(shù)的外層作用域就是組件的methods方法作用域而這個方法在調(diào)用時 this 已經(jīng)綁定到組件實例了所以箭頭函數(shù)里的this就是組件實例。這個差異是解決所有 this 丟失問題的理論基礎(chǔ)。3.2 方法脫離宿主對象的瞬間this 是怎么“脫鉤”的再回到最初那個“傳話人”例子。對象方法的 this 指向宿主對象本質(zhì)原因是“點(diǎn)語法調(diào)用”。當(dāng)你把方法從對象上拆下來const fn obj.say fn()方法離開宿主對象后就不再被對象調(diào)用了。在回調(diào)里同理setTimeout(this.loadData, 1000)表面上你傳入的是this.loadData但這個函數(shù)引用被 setTimeout 拿到后執(zhí)行時和組件實例已經(jīng)沒有綁定關(guān)系了。很多人會把this.loadData誤認(rèn)為“已經(jīng)把 this 傳進(jìn)去了”實際上this.loadData只是在訪問這個函數(shù)真正執(zhí)行的時候函數(shù)體內(nèi)部的this完全由調(diào)用方式?jīng)Q定。整個過程可以拆成三步this.loadData從組件實例上取出函數(shù)引用函數(shù)引用被 setTimeout / Promise / addEventListener 保存異步觸達(dá)后這些內(nèi)部機(jī)制直接調(diào)用函數(shù)沒有經(jīng)過組件實例第三步就是“脫鉤”的瞬間。3.3 為什么 click 不丟 thisVue 幫你做了綁定那 Vue 模板里的clickhandleClick為什么能拿到組件實例因為 Vue 不是簡單地“把函數(shù)引用交給原生事件”它內(nèi)部做了綁定處理在編譯模板時Vue 會把事件處理函數(shù)用invokeWithErrorHandling之類的包裝器包一層并且通過bind把 this 綁定到組件實例上最后才交給addEventListener。簡單說Vue 替你把傳話人帶到了公司門口。但這只在 Vue 自己管理的事件范圍內(nèi)有效。setTimeout、forEach、axios、window.addEventListener這些并不在 Vue 的管轄范圍內(nèi)Vue 不會去給它們做 bind 操作this 該丟還是得丟。4. 找回 this 的四種解法從箭頭函數(shù)到組合式 API4.1 優(yōu)先方案回調(diào)改用箭頭函數(shù)這是最推薦、最優(yōu)雅、最不容易出錯的方案也是日常業(yè)務(wù)里我第一反應(yīng)就會用的寫法methods: { loadData() { setTimeout(() { this.message Hello Vue }, 1000) } }原理很簡單箭頭函數(shù)沒有自己的this它在外層作用域loadData方法作用域找到this。而loadData這個方法本身是被組件實例調(diào)用的所以它的this指向組件實例箭頭函數(shù)繼承了這個this。同理forEach、axios.then、addEventListener的回調(diào)都可以直接用箭頭函數(shù)this.items.forEach((item) { this.checked true }) axios.get(/api/list).then((res) { this.list res.data })但要注意一個邊界箭頭函數(shù)沒有自己的 arguments也不能當(dāng)作構(gòu)造函數(shù)使用如果回調(diào)里需要訪問arguments對象就不要用箭頭函數(shù)。但在 Vue 業(yè)務(wù)代碼里這些場景極少絕大多數(shù)情況箭頭函數(shù)都能直接用。4.2 顯式綁定bind / call / apply 的正確用法如果你寫的是普通函數(shù)又想強(qiáng)制指定 this那就用bind。它會把函數(shù)的 this 永久綁定到你指定的對象上并返回一個新的函數(shù)methods: { loadData() { setTimeout(function () { this.message Hello Vue }.bind(this), 1000) } }bind(this)后面的this是當(dāng)前方法作用域的組件實例。綁定之后setTimeout 內(nèi)部調(diào)用這個函數(shù)時this 不再由調(diào)用者決定而是由 bind 提前鎖死。call和apply是立即執(zhí)行函數(shù)并指定 this 的版本區(qū)別只在傳參方式。比如function greet() { console.log(this.message) } greet.call(componentInstance) // 立即執(zhí)行this 指向 componentInstance greet.apply(componentInstance, args)調(diào)用了bind就會生成一個新函數(shù)所以在移除事件監(jiān)聽的時候要注意用一個變量保存綁定后的函數(shù)引用不能直接用原函數(shù)去移除created() { this.scrollHandler this.handleScroll.bind(this) window.addEventListener(scroll, this.scrollHandler) }, beforeDestroy() { window.removeEventListener(scroll, this.scrollHandler) }4.3 變量緩存用 const vm this 兜底這招是 Vue 時代早期最熱門的寫法現(xiàn)在還能在很多老項目里看到。思路是既然回調(diào)里的this靠不住那就在外層作用域里把 this 存入一個普通變量閉包會把它捕獲下來methods: { loadData() { const vm this setTimeout(function () { vm.message Hello Vue }, 1000) } }這個方案能跑原理也簡單但它沒有解決根本問題只是繞過。我個人的建議是能用箭頭函數(shù)就不要用變量緩存因為變量緩存多了一個中間層代碼可讀性差了一些而且團(tuán)隊成員一多有人不知道vm是什么很容易在重構(gòu)的時候把const vm this刪掉然后線上事故。不過有一個場景它挺管用回調(diào)函數(shù)如果還需要被解綁比如移除事件監(jiān)聽并且你不想用 bind 產(chǎn)生新函數(shù)引用緩存變量能減少心智負(fù)擔(dān)。總之把它當(dāng)兜底方案而不是首選。4.4 終極方案Vue 3 setup 直接繞開 this如果你用的是 Vue 3那我強(qiáng)烈建議直接上組合式 API。在script setup或者setup()函數(shù)里根本沒有this數(shù)據(jù)變量、方法函數(shù)都在同一個作用域中回調(diào)閉包直接訪問變量就好script setup import { ref } from vue const message ref(Hello Vue) function loadData() { setTimeout(() { message.value Hello Composable }, 1000) } loadData() /script這套寫法里沒有this.list、this.loadData只有message變量本身。回調(diào)函數(shù)通過閉包訪問外層的message和this徹底無關(guān)。這也是 Vue 3 組合式 API 設(shè)計上的一個巨大優(yōu)勢從根上消滅了一整類綁定混亂的問題。如果你還在用 Vue 2或者因為歷史原因必須用 Options API那就老老實實把前面三種方法用起來。Vue 3 的 Options API 里this依然存在該丟還是會丟不要以為升級了 Vue 3 這個問題就自動消失。5. 三步排查法快速定位 this 失蹤現(xiàn)場5.1 看報錯信息Cannot read properties 的第一直覺當(dāng)控制臺出現(xiàn)Cannot read properties of undefined (reading xxx)時第一直覺不要馬上懷疑數(shù)據(jù)沒返回而是要懷疑this本身。我見過不少新手在接口回調(diào)里排查半天后端接口怎么查都正常結(jié)果發(fā)現(xiàn)壓根不是接口問題。定位技巧看報錯行號如果代碼行里是this.xxx那大概率 this 丟了如果是res.data.xxx才是數(shù)據(jù)問題。先用這個粗判斷能省下一半排查時間。5.2 打印 this進(jìn)回調(diào)先驗明正身直接在回調(diào)函數(shù)第一行打印this比看任何報錯都快axios.get(/api/list).then(function (res) { console.log(this) // 看看這里到底是什么 })打開控制臺查看如果打印出來是Window說明代碼處于非嚴(yán)格模式函數(shù)被全局環(huán)境調(diào)用如果打印出來是undefined說明處于嚴(yán)格模式如果打印出來是 Vue 組件實例帶$options、$el一堆屬性說明 this 沒丟數(shù)據(jù)問題在別處。這個方法特別適合排查“不報錯但數(shù)據(jù)沒更新”的詭異場景。有時候 this 指向了Window賦值this.list ...并不會報錯但頁面數(shù)據(jù)就是不變這就是 this 丟給了全局對象的表現(xiàn)。5.3 打開調(diào)用??纯词钦l調(diào)用了這個回調(diào)要理解 this 為什么丟最直觀的辦法就是看調(diào)用棧。打開瀏覽器 DevTools → Sources 面板 → 在回調(diào)函數(shù)的代碼行打斷點(diǎn) → 觸發(fā)異步操作 → 觀察右側(cè) Call Stack 列表。你會發(fā)現(xiàn)調(diào)用棧里最先出現(xiàn)的不是你的組件方法而是setTimeout、onLoad、Promise之類的內(nèi)部函數(shù)。這就直接證明了真正調(diào)用這個回調(diào)的是一個內(nèi)部機(jī)制而不是組件實例。這個方法對新手特別有價值它能讓你看清“調(diào)用者”到底是誰而不是停留在死記硬背階段。6. 高頻場景速查表與經(jīng)驗避坑清單6.1 六類高頻場景對照表場景典型寫法this 是否丟失推薦解法setTimeout 定時器setTimeout(function(){ this.xxx }, 1000)丟失箭頭函數(shù) / bindsetInterval 輪詢setInterval(function(){ this.xxx }, 3000)丟失箭頭函數(shù) / bindforEach / map 遍歷arr.forEach(function(item){ this.xxx })丟失箭頭函數(shù)axios / Promise 回調(diào).then(function(res){ this.xxx })丟失箭頭函數(shù)原生事件監(jiān)聽window.addEventListener(scroll, this.handler)丟失bind 保存引用Vue 模板clickclickhandler不丟失不需要處理watch / computed 回調(diào)watch: { a(val) { this.xxx } }不丟失不需要處理6.2 幾個踩過坑之后沉淀的經(jīng)驗第一只要是自己定義的回調(diào)函數(shù)默認(rèn)先寫箭頭函數(shù)除非明確知道它需要動態(tài) this。這能避免絕大多數(shù) this 失蹤案。把這句話當(dāng)成肌肉記憶比記十條規(guī)則更管用。第二定時器回調(diào)里一定要清理定時器。這不僅是 this 丟失的問題還有內(nèi)存泄漏風(fēng)險。在beforeDestroy或onBeforeUnmount里清掉setInterval同時注意移除事件監(jiān)聽時要用綁定后的函數(shù)引用不然移除不掉。第三在 Vue 3 的新代碼里盡量用組合式 API。它把 this 從組件業(yè)務(wù)里徹底踢出去了團(tuán)隊協(xié)作時少一類的爭執(zhí)。老代碼、別人維護(hù)的模塊如果用了 Options API改的時候順手把回調(diào)改成箭頭函數(shù)風(fēng)險最小。第四看別人的代碼時看到普通函數(shù)回調(diào)里用了 this第一時間提高警惕。這不是風(fēng)格問題是可能直接燒掉生產(chǎn)環(huán)境的雷。上次有個同事在setTimeout里寫普通函數(shù)訪問this.$router結(jié)果跳轉(zhuǎn)失敗半天沒查出原因就是 this 丟了。我個人現(xiàn)在寫 Vue 的習(xí)慣已經(jīng)變成能寫script setup絕不用 Options API必須用 Options API 的時候回調(diào)函數(shù)全部箭頭函數(shù)起步遇到第三庫回調(diào)先看文檔對 this 的處理不明確就 bind 一下。這套組合拳打下來已經(jīng)很久沒跟 this 的報錯打過照面了。希望這篇能讓你的“this 失蹤案”也到此為止。